- add verify pin
- restructuring of the backend architecture
This commit is contained in:
parent
c8032cbf62
commit
b3a01a4f16
500
docs/index.html
500
docs/index.html
|
|
@ -766,6 +766,21 @@
|
||||||
"earnedMinutes" : { },
|
"earnedMinutes" : { },
|
||||||
"message" : { }
|
"message" : { }
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
defs["VerifyPinRequest"] = {
|
||||||
|
"title" : "VerifyPinRequest",
|
||||||
|
"required" : [ "pin" ],
|
||||||
|
"properties" : {
|
||||||
|
"pin" : { }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
defs["VerifyPinResponse"] = {
|
||||||
|
"title" : "VerifyPinResponse",
|
||||||
|
"required" : [ "message", "success" ],
|
||||||
|
"properties" : {
|
||||||
|
"success" : { },
|
||||||
|
"message" : { }
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -793,6 +808,9 @@
|
||||||
<li data-group="Default" data-name="apiV1HealthGet" class="">
|
<li data-group="Default" data-name="apiV1HealthGet" class="">
|
||||||
<a href="#api-Default-apiV1HealthGet">apiV1HealthGet</a>
|
<a href="#api-Default-apiV1HealthGet">apiV1HealthGet</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li data-group="Default" data-name="apiV1ParentVerifyPinPost" class="">
|
||||||
|
<a href="#api-Default-apiV1ParentVerifyPinPost">apiV1ParentVerifyPinPost</a>
|
||||||
|
</li>
|
||||||
<li data-group="Default" data-name="apiV1ScreentimeGet" class="">
|
<li data-group="Default" data-name="apiV1ScreentimeGet" class="">
|
||||||
<a href="#api-Default-apiV1ScreentimeGet">apiV1ScreentimeGet</a>
|
<a href="#api-Default-apiV1ScreentimeGet">apiV1ScreentimeGet</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -1630,7 +1648,7 @@ except ApiException as e:
|
||||||
<article id="api-Default-apiV1HealthGet-0" data-group="User" data-name="apiV1HealthGet" data-version="0">
|
<article id="api-Default-apiV1HealthGet-0" data-group="User" data-name="apiV1HealthGet" data-version="0">
|
||||||
<div class="pull-left">
|
<div class="pull-left">
|
||||||
<h1>apiV1HealthGet</h1>
|
<h1>apiV1HealthGet</h1>
|
||||||
<p>Health-Check</p>
|
<p></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right"></div>
|
<div class="pull-right"></div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
@ -1710,7 +1728,6 @@ public class DefaultApiExample {
|
||||||
<pre class="prettyprint"><code class="language-cpp">
|
<pre class="prettyprint"><code class="language-cpp">
|
||||||
DefaultApi *apiInstance = [[DefaultApi alloc] init];
|
DefaultApi *apiInstance = [[DefaultApi alloc] init];
|
||||||
|
|
||||||
// Health-Check
|
|
||||||
[apiInstance apiV1HealthGetWithCompletionHandler:
|
[apiInstance apiV1HealthGetWithCompletionHandler:
|
||||||
^(NSError* error) {
|
^(NSError* error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
@ -1756,7 +1773,6 @@ namespace Example
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Health-Check
|
|
||||||
apiInstance.apiV1HealthGet();
|
apiInstance.apiV1HealthGet();
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
|
@ -1809,7 +1825,6 @@ from pprint import pprint
|
||||||
api_instance = swagger_client.DefaultApi()
|
api_instance = swagger_client.DefaultApi()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Health-Check
|
|
||||||
api_instance.api_v1_health_get()
|
api_instance.api_v1_health_get()
|
||||||
except ApiException as e:
|
except ApiException as e:
|
||||||
print("Exception when calling DefaultApi->apiV1HealthGet: %s\n" % e)</code></pre>
|
print("Exception when calling DefaultApi->apiV1HealthGet: %s\n" % e)</code></pre>
|
||||||
|
|
@ -1876,6 +1891,468 @@ except ApiException as e:
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
<div id="api-Default-apiV1ParentVerifyPinPost">
|
||||||
|
<article id="api-Default-apiV1ParentVerifyPinPost-0" data-group="User" data-name="apiV1ParentVerifyPinPost" data-version="0">
|
||||||
|
<div class="pull-left">
|
||||||
|
<h1>apiV1ParentVerifyPinPost</h1>
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
<div class="pull-right"></div>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
<p></p>
|
||||||
|
<p class="marked"></p>
|
||||||
|
<p></p>
|
||||||
|
<br />
|
||||||
|
<pre class="prettyprint language-html prettyprinted" data-type="post"><code><span class="pln">/api/v1/parent/verify-pin</span></code></pre>
|
||||||
|
<p>
|
||||||
|
<h3>Usage and SDK Samples</h3>
|
||||||
|
</p>
|
||||||
|
<ul class="nav nav-tabs nav-tabs-examples">
|
||||||
|
<li class="active"><a href="#examples-Default-apiV1ParentVerifyPinPost-0-curl">Curl</a></li>
|
||||||
|
<li class=""><a href="#examples-Default-apiV1ParentVerifyPinPost-0-java">Java</a></li>
|
||||||
|
<li class=""><a href="#examples-Default-apiV1ParentVerifyPinPost-0-android">Android</a></li>
|
||||||
|
<!--<li class=""><a href="#examples-Default-apiV1ParentVerifyPinPost-0-groovy">Groovy</a></li>-->
|
||||||
|
<li class=""><a href="#examples-Default-apiV1ParentVerifyPinPost-0-objc">Obj-C</a></li>
|
||||||
|
<li class=""><a href="#examples-Default-apiV1ParentVerifyPinPost-0-javascript">JavaScript</a></li>
|
||||||
|
<!--<li class=""><a href="#examples-Default-apiV1ParentVerifyPinPost-0-angular">Angular</a></li>-->
|
||||||
|
<li class=""><a href="#examples-Default-apiV1ParentVerifyPinPost-0-csharp">C#</a></li>
|
||||||
|
<li class=""><a href="#examples-Default-apiV1ParentVerifyPinPost-0-php">PHP</a></li>
|
||||||
|
<li class=""><a href="#examples-Default-apiV1ParentVerifyPinPost-0-perl">Perl</a></li>
|
||||||
|
<li class=""><a href="#examples-Default-apiV1ParentVerifyPinPost-0-python">Python</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="tab-pane active" id="examples-Default-apiV1ParentVerifyPinPost-0-curl">
|
||||||
|
<pre class="prettyprint"><code class="language-bsh">curl -X POST\
|
||||||
|
-H "Authorization: Bearer [[accessToken]]"\
|
||||||
|
-H "Accept: application/json"\
|
||||||
|
-H "Content-Type: application/json"\
|
||||||
|
"//api/v1/parent/verify-pin"</code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane" id="examples-Default-apiV1ParentVerifyPinPost-0-java">
|
||||||
|
<pre class="prettyprint"><code class="language-java">import io.swagger.client.*;
|
||||||
|
import io.swagger.client.auth.*;
|
||||||
|
import io.swagger.client.model.*;
|
||||||
|
import io.swagger.client.api.DefaultApi;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class DefaultApiExample {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
ApiClient defaultClient = Configuration.getDefaultApiClient();
|
||||||
|
|
||||||
|
|
||||||
|
DefaultApi apiInstance = new DefaultApi();
|
||||||
|
VerifyPinRequest body = ; // VerifyPinRequest |
|
||||||
|
try {
|
||||||
|
VerifyPinResponse result = apiInstance.apiV1ParentVerifyPinPost(body);
|
||||||
|
System.out.println(result);
|
||||||
|
} catch (ApiException e) {
|
||||||
|
System.err.println("Exception when calling DefaultApi#apiV1ParentVerifyPinPost");
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane" id="examples-Default-apiV1ParentVerifyPinPost-0-android">
|
||||||
|
<pre class="prettyprint"><code class="language-java">import io.swagger.client.api.DefaultApi;
|
||||||
|
|
||||||
|
public class DefaultApiExample {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
DefaultApi apiInstance = new DefaultApi();
|
||||||
|
VerifyPinRequest body = ; // VerifyPinRequest |
|
||||||
|
try {
|
||||||
|
VerifyPinResponse result = apiInstance.apiV1ParentVerifyPinPost(body);
|
||||||
|
System.out.println(result);
|
||||||
|
} catch (ApiException e) {
|
||||||
|
System.err.println("Exception when calling DefaultApi#apiV1ParentVerifyPinPost");
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
|
<div class="tab-pane" id="examples-Default-apiV1ParentVerifyPinPost-0-groovy">
|
||||||
|
<pre class="prettyprint language-json prettyprinted" data-type="json"><code>Coming Soon!</code></pre>
|
||||||
|
</div> -->
|
||||||
|
<div class="tab-pane" id="examples-Default-apiV1ParentVerifyPinPost-0-objc">
|
||||||
|
<pre class="prettyprint"><code class="language-cpp">Configuration *apiConfig = [Configuration sharedConfig];
|
||||||
|
VerifyPinRequest *body = ; // (optional)
|
||||||
|
|
||||||
|
DefaultApi *apiInstance = [[DefaultApi alloc] init];
|
||||||
|
|
||||||
|
[apiInstance apiV1ParentVerifyPinPostWith:body
|
||||||
|
completionHandler: ^(VerifyPinResponse output, NSError* error) {
|
||||||
|
if (output) {
|
||||||
|
NSLog(@"%@", output);
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
NSLog(@"Error: %@", error);
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane" id="examples-Default-apiV1ParentVerifyPinPost-0-javascript">
|
||||||
|
<pre class="prettyprint"><code class="language-js">var UntitledApi = require('untitled_api');
|
||||||
|
var defaultClient = UntitledApi.ApiClient.instance;
|
||||||
|
|
||||||
|
|
||||||
|
var api = new UntitledApi.DefaultApi()
|
||||||
|
var opts = {
|
||||||
|
'body': // {{VerifyPinRequest}}
|
||||||
|
};
|
||||||
|
var callback = function(error, data, response) {
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
} else {
|
||||||
|
console.log('API called successfully. Returned data: ' + data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
api.apiV1ParentVerifyPinPost(opts, callback);
|
||||||
|
</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--<div class="tab-pane" id="examples-Default-apiV1ParentVerifyPinPost-0-angular">
|
||||||
|
<pre class="prettyprint language-json prettyprinted" data-type="json"><code>Coming Soon!</code></pre>
|
||||||
|
</div>-->
|
||||||
|
<div class="tab-pane" id="examples-Default-apiV1ParentVerifyPinPost-0-csharp">
|
||||||
|
<pre class="prettyprint"><code class="language-cs">using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using IO.Swagger.Api;
|
||||||
|
using IO.Swagger.Client;
|
||||||
|
using IO.Swagger.Model;
|
||||||
|
|
||||||
|
namespace Example
|
||||||
|
{
|
||||||
|
public class apiV1ParentVerifyPinPostExample
|
||||||
|
{
|
||||||
|
public void main()
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
var apiInstance = new DefaultApi();
|
||||||
|
var body = new VerifyPinRequest(); // VerifyPinRequest | (optional)
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
VerifyPinResponse result = apiInstance.apiV1ParentVerifyPinPost(body);
|
||||||
|
Debug.WriteLine(result);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.Print("Exception when calling DefaultApi.apiV1ParentVerifyPinPost: " + e.Message );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane" id="examples-Default-apiV1ParentVerifyPinPost-0-php">
|
||||||
|
<pre class="prettyprint"><code class="language-php"><?php
|
||||||
|
require_once(__DIR__ . '/vendor/autoload.php');
|
||||||
|
|
||||||
|
|
||||||
|
$api_instance = new Swagger\Client\ApiDefaultApi();
|
||||||
|
$body = ; // VerifyPinRequest |
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $api_instance->apiV1ParentVerifyPinPost($body);
|
||||||
|
print_r($result);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo 'Exception when calling DefaultApi->apiV1ParentVerifyPinPost: ', $e->getMessage(), PHP_EOL;
|
||||||
|
}
|
||||||
|
?></code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane" id="examples-Default-apiV1ParentVerifyPinPost-0-perl">
|
||||||
|
<pre class="prettyprint"><code class="language-perl">use Data::Dumper;
|
||||||
|
use WWW::SwaggerClient::Configuration;
|
||||||
|
use WWW::SwaggerClient::DefaultApi;
|
||||||
|
|
||||||
|
|
||||||
|
my $api_instance = WWW::SwaggerClient::DefaultApi->new();
|
||||||
|
my $body = WWW::SwaggerClient::Object::VerifyPinRequest->new(); # VerifyPinRequest |
|
||||||
|
|
||||||
|
eval {
|
||||||
|
my $result = $api_instance->apiV1ParentVerifyPinPost(body => $body);
|
||||||
|
print Dumper($result);
|
||||||
|
};
|
||||||
|
if ($@) {
|
||||||
|
warn "Exception when calling DefaultApi->apiV1ParentVerifyPinPost: $@\n";
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane" id="examples-Default-apiV1ParentVerifyPinPost-0-python">
|
||||||
|
<pre class="prettyprint"><code class="language-python">from __future__ import print_statement
|
||||||
|
import time
|
||||||
|
import swagger_client
|
||||||
|
from swagger_client.rest import ApiException
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
|
||||||
|
# create an instance of the API class
|
||||||
|
api_instance = swagger_client.DefaultApi()
|
||||||
|
body = # VerifyPinRequest | (optional)
|
||||||
|
|
||||||
|
try:
|
||||||
|
api_response = api_instance.api_v1_parent_verify_pin_post(body=body)
|
||||||
|
pprint(api_response)
|
||||||
|
except ApiException as e:
|
||||||
|
print("Exception when calling DefaultApi->apiV1ParentVerifyPinPost: %s\n" % e)</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Parameters</h2>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="methodsubtabletitle">Body parameters</div>
|
||||||
|
<table id="methodsubtable">
|
||||||
|
<tr>
|
||||||
|
<th width="150px">Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
<tr><td style="width:150px;">body </td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
var schemaWrapper = {
|
||||||
|
"content" : {
|
||||||
|
"application/json" : {
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/VerifyPinRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var schema = schemaWrapper.content["application/json"].schema;
|
||||||
|
if (schema.$ref != null) {
|
||||||
|
schema = defsParser.$refs.get(schema.$ref);
|
||||||
|
} else {
|
||||||
|
schemaWrapper.components = {};
|
||||||
|
schemaWrapper.components.schemas = Object.assign({}, defs);
|
||||||
|
$RefParser.dereference(schemaWrapper).catch(function(err) {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var view = new JSONSchemaView(schema,2,{isBodyParam: true});
|
||||||
|
var result = $('#d2e199_apiV1ParentVerifyPinPost_body');
|
||||||
|
result.empty();
|
||||||
|
result.append(view.render());
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<div id="d2e199_apiV1ParentVerifyPinPost_body"></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<h2>Responses</h2>
|
||||||
|
<h3> Status: 401 - </h3>
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs nav-tabs-examples" >
|
||||||
|
<li class="active">
|
||||||
|
<a data-toggle="tab" href="#responses-apiV1ParentVerifyPinPost-401-schema">Schema</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content" style='margin-bottom: 10px;'>
|
||||||
|
<div class="tab-pane active" id="responses-apiV1ParentVerifyPinPost-401-schema">
|
||||||
|
<div id='responses-apiV1ParentVerifyPinPost-401-schema-401' style="padding: 30px; border-left: 1px solid #eee; border-right: 1px solid #eee; border-bottom: 1px solid #eee;">
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
var schemaWrapper = {
|
||||||
|
"description" : "",
|
||||||
|
"content" : {
|
||||||
|
"application/json" : {
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/VerifyPinResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var schema = schemaWrapper.content["application/json"].schema;
|
||||||
|
if (schema.$ref != null) {
|
||||||
|
schema = defsParser.$refs.get(schema.$ref);
|
||||||
|
} else {
|
||||||
|
schemaWrapper.components = {};
|
||||||
|
schemaWrapper.components.schemas = Object.assign({}, defs);
|
||||||
|
$RefParser.dereference(schemaWrapper).catch(function(err) {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log(JSON.stringify(schema));
|
||||||
|
var view = new JSONSchemaView(schema, 3);
|
||||||
|
$('#responses-apiV1ParentVerifyPinPost-401-schema-data').val(stringify(schema));
|
||||||
|
var result = $('#responses-apiV1ParentVerifyPinPost-401-schema-401');
|
||||||
|
result.empty();
|
||||||
|
result.append(view.render());
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
<input id='responses-apiV1ParentVerifyPinPost-401-schema-data' type='hidden' value=''></input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3> Status: 400 - </h3>
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs nav-tabs-examples" >
|
||||||
|
<li class="active">
|
||||||
|
<a data-toggle="tab" href="#responses-apiV1ParentVerifyPinPost-400-schema">Schema</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content" style='margin-bottom: 10px;'>
|
||||||
|
<div class="tab-pane active" id="responses-apiV1ParentVerifyPinPost-400-schema">
|
||||||
|
<div id='responses-apiV1ParentVerifyPinPost-400-schema-400' style="padding: 30px; border-left: 1px solid #eee; border-right: 1px solid #eee; border-bottom: 1px solid #eee;">
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
var schemaWrapper = {
|
||||||
|
"description" : "",
|
||||||
|
"content" : {
|
||||||
|
"application/json" : {
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/VerifyPinResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var schema = schemaWrapper.content["application/json"].schema;
|
||||||
|
if (schema.$ref != null) {
|
||||||
|
schema = defsParser.$refs.get(schema.$ref);
|
||||||
|
} else {
|
||||||
|
schemaWrapper.components = {};
|
||||||
|
schemaWrapper.components.schemas = Object.assign({}, defs);
|
||||||
|
$RefParser.dereference(schemaWrapper).catch(function(err) {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log(JSON.stringify(schema));
|
||||||
|
var view = new JSONSchemaView(schema, 3);
|
||||||
|
$('#responses-apiV1ParentVerifyPinPost-400-schema-data').val(stringify(schema));
|
||||||
|
var result = $('#responses-apiV1ParentVerifyPinPost-400-schema-400');
|
||||||
|
result.empty();
|
||||||
|
result.append(view.render());
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
<input id='responses-apiV1ParentVerifyPinPost-400-schema-data' type='hidden' value=''></input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3> Status: 404 - </h3>
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs nav-tabs-examples" >
|
||||||
|
<li class="active">
|
||||||
|
<a data-toggle="tab" href="#responses-apiV1ParentVerifyPinPost-404-schema">Schema</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content" style='margin-bottom: 10px;'>
|
||||||
|
<div class="tab-pane active" id="responses-apiV1ParentVerifyPinPost-404-schema">
|
||||||
|
<div id='responses-apiV1ParentVerifyPinPost-404-schema-404' style="padding: 30px; border-left: 1px solid #eee; border-right: 1px solid #eee; border-bottom: 1px solid #eee;">
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
var schemaWrapper = {
|
||||||
|
"description" : "",
|
||||||
|
"content" : {
|
||||||
|
"application/json" : {
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/VerifyPinResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var schema = schemaWrapper.content["application/json"].schema;
|
||||||
|
if (schema.$ref != null) {
|
||||||
|
schema = defsParser.$refs.get(schema.$ref);
|
||||||
|
} else {
|
||||||
|
schemaWrapper.components = {};
|
||||||
|
schemaWrapper.components.schemas = Object.assign({}, defs);
|
||||||
|
$RefParser.dereference(schemaWrapper).catch(function(err) {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log(JSON.stringify(schema));
|
||||||
|
var view = new JSONSchemaView(schema, 3);
|
||||||
|
$('#responses-apiV1ParentVerifyPinPost-404-schema-data').val(stringify(schema));
|
||||||
|
var result = $('#responses-apiV1ParentVerifyPinPost-404-schema-404');
|
||||||
|
result.empty();
|
||||||
|
result.append(view.render());
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
<input id='responses-apiV1ParentVerifyPinPost-404-schema-data' type='hidden' value=''></input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3> Status: 200 - </h3>
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs nav-tabs-examples" >
|
||||||
|
<li class="active">
|
||||||
|
<a data-toggle="tab" href="#responses-apiV1ParentVerifyPinPost-200-schema">Schema</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content" style='margin-bottom: 10px;'>
|
||||||
|
<div class="tab-pane active" id="responses-apiV1ParentVerifyPinPost-200-schema">
|
||||||
|
<div id='responses-apiV1ParentVerifyPinPost-200-schema-200' style="padding: 30px; border-left: 1px solid #eee; border-right: 1px solid #eee; border-bottom: 1px solid #eee;">
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
var schemaWrapper = {
|
||||||
|
"description" : "",
|
||||||
|
"content" : {
|
||||||
|
"application/json" : {
|
||||||
|
"schema" : {
|
||||||
|
"$ref" : "#/components/schemas/VerifyPinResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var schema = schemaWrapper.content["application/json"].schema;
|
||||||
|
if (schema.$ref != null) {
|
||||||
|
schema = defsParser.$refs.get(schema.$ref);
|
||||||
|
} else {
|
||||||
|
schemaWrapper.components = {};
|
||||||
|
schemaWrapper.components.schemas = Object.assign({}, defs);
|
||||||
|
$RefParser.dereference(schemaWrapper).catch(function(err) {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log(JSON.stringify(schema));
|
||||||
|
var view = new JSONSchemaView(schema, 3);
|
||||||
|
$('#responses-apiV1ParentVerifyPinPost-200-schema-data').val(stringify(schema));
|
||||||
|
var result = $('#responses-apiV1ParentVerifyPinPost-200-schema-200');
|
||||||
|
result.empty();
|
||||||
|
result.append(view.render());
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
<input id='responses-apiV1ParentVerifyPinPost-200-schema-data' type='hidden' value=''></input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
<div id="api-Default-apiV1ScreentimeGet">
|
<div id="api-Default-apiV1ScreentimeGet">
|
||||||
<article id="api-Default-apiV1ScreentimeGet-0" data-group="User" data-name="apiV1ScreentimeGet" data-version="0">
|
<article id="api-Default-apiV1ScreentimeGet-0" data-group="User" data-name="apiV1ScreentimeGet" data-version="0">
|
||||||
<div class="pull-left">
|
<div class="pull-left">
|
||||||
|
|
@ -2194,7 +2671,7 @@ except ApiException as e:
|
||||||
<article id="api-Default-apiV1ScreentimeSyncPost-0" data-group="User" data-name="apiV1ScreentimeSyncPost" data-version="0">
|
<article id="api-Default-apiV1ScreentimeSyncPost-0" data-group="User" data-name="apiV1ScreentimeSyncPost" data-version="0">
|
||||||
<div class="pull-left">
|
<div class="pull-left">
|
||||||
<h1>apiV1ScreentimeSyncPost</h1>
|
<h1>apiV1ScreentimeSyncPost</h1>
|
||||||
<p>3. Bildschirmzeit synchronisieren</p>
|
<p></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right"></div>
|
<div class="pull-right"></div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
@ -2284,7 +2761,6 @@ SyncRequest *body = ; // (optional)
|
||||||
|
|
||||||
DefaultApi *apiInstance = [[DefaultApi alloc] init];
|
DefaultApi *apiInstance = [[DefaultApi alloc] init];
|
||||||
|
|
||||||
// 3. Bildschirmzeit synchronisieren
|
|
||||||
[apiInstance apiV1ScreentimeSyncPostWith:body
|
[apiInstance apiV1ScreentimeSyncPostWith:body
|
||||||
completionHandler: ^(SyncResponse output, NSError* error) {
|
completionHandler: ^(SyncResponse output, NSError* error) {
|
||||||
if (output) {
|
if (output) {
|
||||||
|
|
@ -2340,7 +2816,6 @@ namespace Example
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 3. Bildschirmzeit synchronisieren
|
|
||||||
SyncResponse result = apiInstance.apiV1ScreentimeSyncPost(body);
|
SyncResponse result = apiInstance.apiV1ScreentimeSyncPost(body);
|
||||||
Debug.WriteLine(result);
|
Debug.WriteLine(result);
|
||||||
}
|
}
|
||||||
|
|
@ -2402,7 +2877,6 @@ api_instance = swagger_client.DefaultApi()
|
||||||
body = # SyncRequest | (optional)
|
body = # SyncRequest | (optional)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 3. Bildschirmzeit synchronisieren
|
|
||||||
api_response = api_instance.api_v1_screentime_sync_post(body=body)
|
api_response = api_instance.api_v1_screentime_sync_post(body=body)
|
||||||
pprint(api_response)
|
pprint(api_response)
|
||||||
except ApiException as e:
|
except ApiException as e:
|
||||||
|
|
@ -2515,7 +2989,7 @@ except ApiException as e:
|
||||||
<article id="api-Default-apiV1TasksGet-0" data-group="User" data-name="apiV1TasksGet" data-version="0">
|
<article id="api-Default-apiV1TasksGet-0" data-group="User" data-name="apiV1TasksGet" data-version="0">
|
||||||
<div class="pull-left">
|
<div class="pull-left">
|
||||||
<h1>apiV1TasksGet</h1>
|
<h1>apiV1TasksGet</h1>
|
||||||
<p>1. Alle verfügbaren Aufgaben abrufen (mit optionaler Filterung nach Fach und Alter)</p>
|
<p></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right"></div>
|
<div class="pull-right"></div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
@ -2608,7 +3082,6 @@ public class DefaultApiExample {
|
||||||
|
|
||||||
DefaultApi *apiInstance = [[DefaultApi alloc] init];
|
DefaultApi *apiInstance = [[DefaultApi alloc] init];
|
||||||
|
|
||||||
// 1. Alle verfügbaren Aufgaben abrufen (mit optionaler Filterung nach Fach und Alter)
|
|
||||||
[apiInstance apiV1TasksGetWith:subject
|
[apiInstance apiV1TasksGetWith:subject
|
||||||
age:age
|
age:age
|
||||||
limit:limit
|
limit:limit
|
||||||
|
|
@ -2667,7 +3140,6 @@ namespace Example
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 1. Alle verfügbaren Aufgaben abrufen (mit optionaler Filterung nach Fach und Alter)
|
|
||||||
apiInstance.apiV1TasksGet(subject, age, limit);
|
apiInstance.apiV1TasksGet(subject, age, limit);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
|
@ -2732,7 +3204,6 @@ age = # | (optional)
|
||||||
limit = # | (optional)
|
limit = # | (optional)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. Alle verfügbaren Aufgaben abrufen (mit optionaler Filterung nach Fach und Alter)
|
|
||||||
api_instance.api_v1_tasks_get(subject=subject, age=age, limit=limit)
|
api_instance.api_v1_tasks_get(subject=subject, age=age, limit=limit)
|
||||||
except ApiException as e:
|
except ApiException as e:
|
||||||
print("Exception when calling DefaultApi->apiV1TasksGet: %s\n" % e)</code></pre>
|
print("Exception when calling DefaultApi->apiV1TasksGet: %s\n" % e)</code></pre>
|
||||||
|
|
@ -2860,7 +3331,7 @@ except ApiException as e:
|
||||||
<article id="api-Default-apiV1TasksVerifyPost-0" data-group="User" data-name="apiV1TasksVerifyPost" data-version="0">
|
<article id="api-Default-apiV1TasksVerifyPost-0" data-group="User" data-name="apiV1TasksVerifyPost" data-version="0">
|
||||||
<div class="pull-left">
|
<div class="pull-left">
|
||||||
<h1>apiV1TasksVerifyPost</h1>
|
<h1>apiV1TasksVerifyPost</h1>
|
||||||
<p>2. Antwort auf eine Aufgabe überprüfen</p>
|
<p></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right"></div>
|
<div class="pull-right"></div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
@ -2950,7 +3421,6 @@ TaskVerifyRequest *body = ; // (optional)
|
||||||
|
|
||||||
DefaultApi *apiInstance = [[DefaultApi alloc] init];
|
DefaultApi *apiInstance = [[DefaultApi alloc] init];
|
||||||
|
|
||||||
// 2. Antwort auf eine Aufgabe überprüfen
|
|
||||||
[apiInstance apiV1TasksVerifyPostWith:body
|
[apiInstance apiV1TasksVerifyPostWith:body
|
||||||
completionHandler: ^(TaskVerifyResponse output, NSError* error) {
|
completionHandler: ^(TaskVerifyResponse output, NSError* error) {
|
||||||
if (output) {
|
if (output) {
|
||||||
|
|
@ -3006,7 +3476,6 @@ namespace Example
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 2. Antwort auf eine Aufgabe überprüfen
|
|
||||||
TaskVerifyResponse result = apiInstance.apiV1TasksVerifyPost(body);
|
TaskVerifyResponse result = apiInstance.apiV1TasksVerifyPost(body);
|
||||||
Debug.WriteLine(result);
|
Debug.WriteLine(result);
|
||||||
}
|
}
|
||||||
|
|
@ -3068,7 +3537,6 @@ api_instance = swagger_client.DefaultApi()
|
||||||
body = # TaskVerifyRequest | (optional)
|
body = # TaskVerifyRequest | (optional)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 2. Antwort auf eine Aufgabe überprüfen
|
|
||||||
api_response = api_instance.api_v1_tasks_verify_post(body=body)
|
api_response = api_instance.api_v1_tasks_verify_post(body=body)
|
||||||
pprint(api_response)
|
pprint(api_response)
|
||||||
except ApiException as e:
|
except ApiException as e:
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,14 @@
|
||||||
package com.oliver.kidio.backend.data.database
|
package com.oliver.kidio.backend.data.database
|
||||||
|
|
||||||
|
import com.oliver.kidio.backend.data.tables.ScreentimeTable
|
||||||
|
import com.oliver.kidio.backend.data.tables.TasksTable
|
||||||
|
import com.oliver.kidio.backend.data.tables.UserTable
|
||||||
import io.ktor.server.config.*
|
import io.ktor.server.config.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.jetbrains.exposed.sql.*
|
import org.jetbrains.exposed.sql.*
|
||||||
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
|
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
object TasksTable : Table("tasks") {
|
|
||||||
val id = varchar("id", 64)
|
|
||||||
val externalSource = varchar("external_source", 32).default("local")
|
|
||||||
val sourceUrl = text("source_url").nullable()
|
|
||||||
val license = varchar("license", 100).default("CC BY-SA 4.0")
|
|
||||||
val subject = varchar("subject", 64).default("Mathematik")
|
|
||||||
val minAge = integer("min_age").default(6)
|
|
||||||
val maxAge = integer("max_age").default(12)
|
|
||||||
val taskType = varchar("task_type", 32).default("MULTIPLE_CHOICE")
|
|
||||||
val question = text("question")
|
|
||||||
val options = text("options")
|
|
||||||
val correctAnswer = varchar("correct_answer", 255)
|
|
||||||
val rewardMinutes = integer("reward_minutes").default(5)
|
|
||||||
|
|
||||||
override val primaryKey = PrimaryKey(id)
|
|
||||||
|
|
||||||
init {
|
|
||||||
index("idx_tasks_filter", false, subject, minAge, maxAge)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object ScreentimeTable : Table("user_screentime") {
|
|
||||||
val userId = varchar("user_id", 50)
|
|
||||||
val remainingSeconds = integer("remaining_seconds")
|
|
||||||
|
|
||||||
override val primaryKey = PrimaryKey(userId)
|
|
||||||
}
|
|
||||||
|
|
||||||
object UserTable : Table("users") {
|
|
||||||
val id = varchar("id", 36)
|
|
||||||
val email = varchar("email", 255).uniqueIndex()
|
|
||||||
val passwordHash = varchar("password_hash", 255)
|
|
||||||
val familyName = varchar("family_name", 100).default("Familie")
|
|
||||||
val childAge = integer("child_age").default(8)
|
|
||||||
val parentPin = varchar("parent_pin", 4).default("1234")
|
|
||||||
|
|
||||||
override val primaryKey = PrimaryKey(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
object DatabaseFactory {
|
object DatabaseFactory {
|
||||||
fun init(config: ApplicationConfig) {
|
fun init(config: ApplicationConfig) {
|
||||||
val driverClassName = config.propertyOrNull("storage.driverClassName")?.getString() ?: "org.postgresql.Driver"
|
val driverClassName = config.propertyOrNull("storage.driverClassName")?.getString() ?: "org.postgresql.Driver"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.oliver.kidio.backend.data.tables
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.sql.Table
|
||||||
|
|
||||||
|
object ScreentimeTable : Table("user_screentime") {
|
||||||
|
val userId = varchar("user_id", 50)
|
||||||
|
val remainingSeconds = integer("remaining_seconds")
|
||||||
|
|
||||||
|
override val primaryKey = PrimaryKey(userId)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.oliver.kidio.backend.data.tables
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.sql.Table
|
||||||
|
|
||||||
|
object TasksTable : Table("tasks") {
|
||||||
|
val id = varchar("id", 64)
|
||||||
|
val externalSource = varchar("external_source", 32).default("local")
|
||||||
|
val sourceUrl = text("source_url").nullable()
|
||||||
|
val license = varchar("license", 100).default("CC BY-SA 4.0")
|
||||||
|
val subject = varchar("subject", 64).default("Mathematik")
|
||||||
|
val minAge = integer("min_age").default(6)
|
||||||
|
val maxAge = integer("max_age").default(12)
|
||||||
|
val taskType = varchar("task_type", 32).default("MULTIPLE_CHOICE")
|
||||||
|
val question = text("question")
|
||||||
|
val options = text("options")
|
||||||
|
val correctAnswer = varchar("correct_answer", 255)
|
||||||
|
val rewardMinutes = integer("reward_minutes").default(5)
|
||||||
|
|
||||||
|
override val primaryKey = PrimaryKey(id)
|
||||||
|
|
||||||
|
init {
|
||||||
|
index("idx_tasks_filter", false, subject, minAge, maxAge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.oliver.kidio.backend.data.tables
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.sql.Table
|
||||||
|
|
||||||
|
object UserTable : Table("users") {
|
||||||
|
val id = varchar("id", 36)
|
||||||
|
val email = varchar("email", 255).uniqueIndex()
|
||||||
|
val passwordHash = varchar("password_hash", 255)
|
||||||
|
val familyName = varchar("family_name", 100).default("Familie")
|
||||||
|
val childAge = integer("child_age").default(8)
|
||||||
|
val parentPin = varchar("parent_pin", 4).default("1234")
|
||||||
|
|
||||||
|
override val primaryKey = PrimaryKey(id)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.oliver.kidio.backend.domain.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RegisterRequest(
|
||||||
|
val email: String,
|
||||||
|
val passwordPlain: String,
|
||||||
|
val familyName: String,
|
||||||
|
val childAge: Int,
|
||||||
|
val parentPin: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LoginRequest(
|
||||||
|
val email: String,
|
||||||
|
val passwordPlain: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AuthResponse(
|
||||||
|
val token: String,
|
||||||
|
val userId: String,
|
||||||
|
val familyName: String,
|
||||||
|
val childAge: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class VerifyPinRequest(
|
||||||
|
val pin: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class VerifyPinResponse(
|
||||||
|
val success: Boolean,
|
||||||
|
val message: String,
|
||||||
|
)
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.oliver.kidio.backend
|
package com.oliver.kidio.backend.domain.models
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
|
@ -45,51 +45,3 @@ data class TaskVerifyResponse(
|
||||||
val earnedMinutes: Int,
|
val earnedMinutes: Int,
|
||||||
val message: String,
|
val message: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Datenabgleich für die Bildschirmzeit (Sync)
|
|
||||||
@Serializable
|
|
||||||
data class SyncRequest(
|
|
||||||
val userId: String,
|
|
||||||
val usedSecondsSinceLastSync: Int,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SyncResponse(
|
|
||||||
val remainingSeconds: Int,
|
|
||||||
val isBlocked: Boolean,
|
|
||||||
)
|
|
||||||
|
|
||||||
// --- NEU: AUTHENTIFIZIERUNG ---
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class RegisterRequest(
|
|
||||||
val email: String,
|
|
||||||
val passwordPlain: String,
|
|
||||||
val familyName: String,
|
|
||||||
val childAge: Int,
|
|
||||||
val parentPin: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class LoginRequest(
|
|
||||||
val email: String,
|
|
||||||
val passwordPlain: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class AuthResponse(
|
|
||||||
val token: String,
|
|
||||||
val userId: String,
|
|
||||||
val familyName: String,
|
|
||||||
val childAge: Int,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Interne Backend-Klasse (Domain Model).
|
|
||||||
data class User(
|
|
||||||
val id: String,
|
|
||||||
val email: String,
|
|
||||||
val passwordHash: String,
|
|
||||||
val familyName: String,
|
|
||||||
val childAge: Int,
|
|
||||||
val parentPin: String,
|
|
||||||
)
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.oliver.kidio.backend.domain.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
// Interne Backend-Klasse (Domain Model).
|
||||||
|
data class User(
|
||||||
|
val id: String,
|
||||||
|
val email: String,
|
||||||
|
val passwordHash: String,
|
||||||
|
val familyName: String,
|
||||||
|
val childAge: Int,
|
||||||
|
val parentPin: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Datenabgleich für die Bildschirmzeit (Sync)
|
||||||
|
@Serializable
|
||||||
|
data class SyncRequest(
|
||||||
|
val userId: String,
|
||||||
|
val usedSecondsSinceLastSync: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SyncResponse(
|
||||||
|
val remainingSeconds: Int,
|
||||||
|
val isBlocked: Boolean,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,242 @@
|
||||||
|
package com.oliver.kidio.backend.domain.repository
|
||||||
|
|
||||||
|
import com.oliver.kidio.backend.data.database.DatabaseFactory
|
||||||
|
import com.oliver.kidio.backend.data.database.RedisFactory
|
||||||
|
import com.oliver.kidio.backend.data.tables.ScreentimeTable
|
||||||
|
import com.oliver.kidio.backend.data.tables.TasksTable
|
||||||
|
import com.oliver.kidio.backend.data.tables.UserTable
|
||||||
|
import com.oliver.kidio.backend.domain.models.ImportedTask
|
||||||
|
import com.oliver.kidio.backend.domain.models.Task
|
||||||
|
import com.oliver.kidio.backend.domain.models.User
|
||||||
|
import org.jetbrains.exposed.sql.*
|
||||||
|
import org.mindrot.jbcrypt.BCrypt
|
||||||
|
|
||||||
|
class ExposedKidioRepository : KidioRepository {
|
||||||
|
override suspend fun getTasks(
|
||||||
|
subject: String?,
|
||||||
|
age: Int?,
|
||||||
|
limit: Int,
|
||||||
|
): List<Task> =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
|
var query = TasksTable.selectAll()
|
||||||
|
if (!subject.isNullOrBlank()) {
|
||||||
|
query = query.where { TasksTable.subject.lowerCase() eq subject.trim().lowercase() }
|
||||||
|
}
|
||||||
|
if (age != null) {
|
||||||
|
query = query.andWhere { (TasksTable.minAge lessEq age) and (TasksTable.maxAge greaterEq age) }
|
||||||
|
}
|
||||||
|
val safeLimit = if (limit <= 0) 10 else limit
|
||||||
|
query.limit(safeLimit).map {
|
||||||
|
Task(
|
||||||
|
id = it[TasksTable.id],
|
||||||
|
subject = it[TasksTable.subject],
|
||||||
|
minAge = it[TasksTable.minAge],
|
||||||
|
maxAge = it[TasksTable.maxAge],
|
||||||
|
question = it[TasksTable.question],
|
||||||
|
options = it[TasksTable.options].split(",").map { opt -> opt.trim() },
|
||||||
|
rewardMinutes = it[TasksTable.rewardMinutes],
|
||||||
|
license = it[TasksTable.license],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getUserById(userId: String): User? =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
|
UserTable.selectAll()
|
||||||
|
.where { UserTable.id eq userId }
|
||||||
|
.map {
|
||||||
|
User(
|
||||||
|
id = it[UserTable.id],
|
||||||
|
email = it[UserTable.email],
|
||||||
|
passwordHash = it[UserTable.passwordHash],
|
||||||
|
familyName = it[UserTable.familyName],
|
||||||
|
childAge = it[UserTable.childAge],
|
||||||
|
parentPin = it[UserTable.parentPin],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.singleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAllTasks(): List<Task> = getTasks(limit = 100)
|
||||||
|
|
||||||
|
override suspend fun getExistingTaskIds(externalSource: String): Set<String> =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
|
TasksTable
|
||||||
|
.select(TasksTable.id)
|
||||||
|
.where { TasksTable.externalSource eq externalSource }
|
||||||
|
.map { it[TasksTable.id] }
|
||||||
|
.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun upsertTask(task: ImportedTask): Boolean =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
|
try {
|
||||||
|
val updated =
|
||||||
|
TasksTable.update({ TasksTable.id eq task.id }) {
|
||||||
|
it[externalSource] = task.externalSource
|
||||||
|
it[sourceUrl] = task.sourceUrl
|
||||||
|
it[license] = task.license
|
||||||
|
it[subject] = task.subject
|
||||||
|
it[minAge] = task.minAge
|
||||||
|
it[maxAge] = task.maxAge
|
||||||
|
it[taskType] = task.taskType
|
||||||
|
it[question] = task.question
|
||||||
|
it[options] = task.options.joinToString(",")
|
||||||
|
it[correctAnswer] = task.correctAnswer
|
||||||
|
it[rewardMinutes] = task.rewardMinutes
|
||||||
|
}
|
||||||
|
if (updated == 0) {
|
||||||
|
TasksTable.insert {
|
||||||
|
it[id] = task.id
|
||||||
|
it[externalSource] = task.externalSource
|
||||||
|
it[sourceUrl] = task.sourceUrl
|
||||||
|
it[license] = task.license
|
||||||
|
it[subject] = task.subject
|
||||||
|
it[minAge] = task.minAge
|
||||||
|
it[maxAge] = task.maxAge
|
||||||
|
it[taskType] = task.taskType
|
||||||
|
it[question] = task.question
|
||||||
|
it[options] = task.options.joinToString(",")
|
||||||
|
it[correctAnswer] = task.correctAnswer
|
||||||
|
it[rewardMinutes] = task.rewardMinutes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("FEHLER beim Upsert von Task ${task.id}: ${e.message}")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun registerUser(user: User): Boolean =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
|
try {
|
||||||
|
UserTable.insert {
|
||||||
|
it[id] = user.id
|
||||||
|
it[email] = user.email
|
||||||
|
it[passwordHash] = user.passwordHash
|
||||||
|
it[familyName] = user.familyName
|
||||||
|
it[childAge] = user.childAge
|
||||||
|
it[parentPin] = user.parentPin
|
||||||
|
}
|
||||||
|
ScreentimeTable.insert {
|
||||||
|
it[userId] = user.id
|
||||||
|
it[remainingSeconds] = 1800 // Startguthaben: 30 Minuten
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("FEHLER bei Registrierung: ${e.message}")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>? =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
|
TasksTable.selectAll()
|
||||||
|
.where { TasksTable.id eq taskId }
|
||||||
|
.map { it[TasksTable.correctAnswer] to it[TasksTable.rewardMinutes] }
|
||||||
|
.singleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getScreentime(userId: String): Int {
|
||||||
|
val redisKey = "screentime:$userId"
|
||||||
|
|
||||||
|
try {
|
||||||
|
RedisFactory.getResource().use { jedis ->
|
||||||
|
val cachedSeconds = jedis.get(redisKey)
|
||||||
|
if (cachedSeconds != null) {
|
||||||
|
println("DEBUG: Bildschirmzeit für $userId aus REDIS geladen: $cachedSeconds Sek")
|
||||||
|
return cachedSeconds.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("WARNUNG: Konnte nicht aus Redis lesen (Postgres-Fallback genutzt): ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val dbSeconds =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
|
ScreentimeTable.selectAll()
|
||||||
|
.where { ScreentimeTable.userId eq userId }
|
||||||
|
.map { it[ScreentimeTable.remainingSeconds] }
|
||||||
|
.singleOrNull() ?: 1800
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
RedisFactory.getResource().use { jedis ->
|
||||||
|
jedis.setex(redisKey, 3600, dbSeconds.toString())
|
||||||
|
println("DEBUG: Bildschirmzeit für $userId in REDIS gecacht.")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("WARNUNG: Konnte Wert nicht in Redis speichern: ${e.message}")
|
||||||
|
}
|
||||||
|
return dbSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updateScreentime(
|
||||||
|
userId: String,
|
||||||
|
seconds: Int,
|
||||||
|
) {
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
|
val rowsUpdated =
|
||||||
|
ScreentimeTable.update({ ScreentimeTable.userId eq userId }) {
|
||||||
|
it[remainingSeconds] = seconds
|
||||||
|
}
|
||||||
|
if (rowsUpdated == 0) {
|
||||||
|
ScreentimeTable.insert {
|
||||||
|
it[ScreentimeTable.userId] = userId
|
||||||
|
it[ScreentimeTable.remainingSeconds] = seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val redisKey = "screentime:$userId"
|
||||||
|
try {
|
||||||
|
RedisFactory.getResource().use { jedis ->
|
||||||
|
jedis.setex(redisKey, 3600, seconds.toString())
|
||||||
|
println("DEBUG: Bildschirmzeit für $userId in REDIS aktualisiert auf $seconds Sek")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("WARNUNG: Konnte Redis nicht aktualisieren: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByEmail(email: String): User? =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
|
UserTable.selectAll()
|
||||||
|
.where { UserTable.email eq email }
|
||||||
|
.map {
|
||||||
|
User(
|
||||||
|
id = it[UserTable.id],
|
||||||
|
email = it[UserTable.email],
|
||||||
|
passwordHash = it[UserTable.passwordHash],
|
||||||
|
familyName = it[UserTable.familyName],
|
||||||
|
childAge = it[UserTable.childAge],
|
||||||
|
parentPin = it[UserTable.parentPin],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.singleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findById(userId: String): User? =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
|
UserTable.selectAll()
|
||||||
|
.where { UserTable.id eq userId }
|
||||||
|
.map {
|
||||||
|
User(
|
||||||
|
id = it[UserTable.id],
|
||||||
|
email = it[UserTable.email],
|
||||||
|
passwordHash = it[UserTable.passwordHash],
|
||||||
|
familyName = it[UserTable.familyName],
|
||||||
|
childAge = it[UserTable.childAge],
|
||||||
|
parentPin = it[UserTable.parentPin],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.singleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun verifyPassword(
|
||||||
|
password: String,
|
||||||
|
passwordHash: String,
|
||||||
|
): Boolean {
|
||||||
|
return BCrypt.checkpw(password, passwordHash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
package com.oliver.kidio.backend.domain.repository
|
||||||
|
|
||||||
|
import com.oliver.kidio.backend.domain.models.ImportedTask
|
||||||
|
import com.oliver.kidio.backend.domain.models.Task
|
||||||
|
import com.oliver.kidio.backend.domain.models.User
|
||||||
|
import org.mindrot.jbcrypt.BCrypt
|
||||||
|
|
||||||
|
class InMemoryKidioRepository : KidioRepository {
|
||||||
|
private val tasks =
|
||||||
|
mutableListOf(
|
||||||
|
ImportedTask(
|
||||||
|
id = "t1",
|
||||||
|
externalSource = "manual",
|
||||||
|
subject = "Mathematik",
|
||||||
|
minAge = 6,
|
||||||
|
maxAge = 10,
|
||||||
|
taskType = "MULTIPLE_CHOICE",
|
||||||
|
question = "Was ist 12 + 15?",
|
||||||
|
options = listOf("25", "27", "30", "22"),
|
||||||
|
correctAnswer = "27",
|
||||||
|
rewardMinutes = 10,
|
||||||
|
license = "CC BY-SA 4.0",
|
||||||
|
sourceUrl = "local",
|
||||||
|
),
|
||||||
|
ImportedTask(
|
||||||
|
id = "t2",
|
||||||
|
externalSource = "manual",
|
||||||
|
subject = "Mathematik",
|
||||||
|
minAge = 7,
|
||||||
|
maxAge = 12,
|
||||||
|
taskType = "MULTIPLE_CHOICE",
|
||||||
|
question = "Was ist 8 x 7?",
|
||||||
|
options = listOf("54", "56", "64", "48"),
|
||||||
|
correctAnswer = "56",
|
||||||
|
rewardMinutes = 15,
|
||||||
|
license = "CC BY-SA 4.0",
|
||||||
|
sourceUrl = "local",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
private val screentimes =
|
||||||
|
mutableMapOf(
|
||||||
|
"default_user" to 1800,
|
||||||
|
)
|
||||||
|
private val users = mutableListOf<User>()
|
||||||
|
|
||||||
|
override suspend fun getAllTasks(): List<Task> = getTasks(limit = 100)
|
||||||
|
|
||||||
|
override suspend fun getTasks(
|
||||||
|
subject: String?,
|
||||||
|
age: Int?,
|
||||||
|
limit: Int,
|
||||||
|
): List<Task> {
|
||||||
|
val safeLimit = if (limit <= 0) 10 else limit
|
||||||
|
return tasks.filter { task ->
|
||||||
|
val matchSubject = subject.isNullOrBlank() || task.subject.equals(subject.trim(), ignoreCase = true)
|
||||||
|
val matchAge = age == null || (task.minAge <= age && task.maxAge >= age)
|
||||||
|
matchSubject && matchAge
|
||||||
|
}.take(safeLimit).map {
|
||||||
|
Task(
|
||||||
|
id = it.id,
|
||||||
|
subject = it.subject,
|
||||||
|
minAge = it.minAge,
|
||||||
|
maxAge = it.maxAge,
|
||||||
|
question = it.question,
|
||||||
|
options = it.options,
|
||||||
|
rewardMinutes = it.rewardMinutes,
|
||||||
|
license = it.license,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getUserById(userId: String): User? {
|
||||||
|
return users.find { it.id == userId }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getExistingTaskIds(externalSource: String): Set<String> {
|
||||||
|
return tasks
|
||||||
|
.filter { it.externalSource == externalSource }
|
||||||
|
.map { it.id }
|
||||||
|
.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun upsertTask(task: ImportedTask): Boolean {
|
||||||
|
tasks.removeIf { it.id == task.id }
|
||||||
|
tasks.add(task)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>? {
|
||||||
|
val task = tasks.find { it.id == taskId } ?: return null
|
||||||
|
return task.correctAnswer to task.rewardMinutes
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getScreentime(userId: String): Int = screentimes[userId] ?: 1800
|
||||||
|
|
||||||
|
override suspend fun updateScreentime(
|
||||||
|
userId: String,
|
||||||
|
seconds: Int,
|
||||||
|
) {
|
||||||
|
screentimes[userId] = seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByEmail(email: String): User? {
|
||||||
|
return users.find { it.email == email }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findById(userId: String): User? {
|
||||||
|
return users.find { it.id == userId }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun verifyPassword(
|
||||||
|
password: String,
|
||||||
|
passwordHash: String,
|
||||||
|
): Boolean {
|
||||||
|
return BCrypt.checkpw(password, passwordHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun registerUser(user: User): Boolean {
|
||||||
|
users.add(user)
|
||||||
|
screentimes[user.id] = 1800
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,32 +1,22 @@
|
||||||
package com.oliver.kidio.backend.domain.repository
|
package com.oliver.kidio.backend.domain.repository
|
||||||
|
|
||||||
import com.oliver.kidio.backend.ImportedTask
|
import com.oliver.kidio.backend.domain.models.ImportedTask
|
||||||
import com.oliver.kidio.backend.Task
|
import com.oliver.kidio.backend.domain.models.Task
|
||||||
import com.oliver.kidio.backend.User
|
import com.oliver.kidio.backend.domain.models.User
|
||||||
import com.oliver.kidio.backend.data.database.DatabaseFactory
|
|
||||||
import com.oliver.kidio.backend.data.database.RedisFactory
|
|
||||||
import com.oliver.kidio.backend.data.database.ScreentimeTable
|
|
||||||
import com.oliver.kidio.backend.data.database.TasksTable
|
|
||||||
import com.oliver.kidio.backend.data.database.UserTable
|
|
||||||
import org.jetbrains.exposed.sql.*
|
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greaterEq
|
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq
|
|
||||||
import org.jetbrains.exposed.sql.lowerCase
|
|
||||||
import org.mindrot.jbcrypt.BCrypt
|
|
||||||
|
|
||||||
interface KidioRepository {
|
interface KidioRepository {
|
||||||
suspend fun getAllTasks(): List<Task> = getTasks(limit = 100)
|
suspend fun getAllTasks(): List<Task> = getTasks(limit = 100)
|
||||||
|
|
||||||
suspend fun getUserById(userId: String): User?
|
suspend fun getUserById(userId: String): User?
|
||||||
|
|
||||||
|
suspend fun findById(userId: String): User?
|
||||||
|
|
||||||
suspend fun getTasks(
|
suspend fun getTasks(
|
||||||
subject: String? = null,
|
subject: String? = null,
|
||||||
age: Int? = null,
|
age: Int? = null,
|
||||||
limit: Int = 10,
|
limit: Int = 10,
|
||||||
): List<Task>
|
): List<Task>
|
||||||
|
|
||||||
// Mit suspend markiert, damit es zur DatabaseFactory.dbQuery Architektur passt
|
|
||||||
suspend fun getExistingTaskIds(externalSource: String = "zum.de"): Set<String>
|
suspend fun getExistingTaskIds(externalSource: String = "zum.de"): Set<String>
|
||||||
|
|
||||||
suspend fun upsertTask(task: ImportedTask): Boolean
|
suspend fun upsertTask(task: ImportedTask): Boolean
|
||||||
|
|
@ -49,331 +39,3 @@ interface KidioRepository {
|
||||||
|
|
||||||
suspend fun registerUser(user: User): Boolean
|
suspend fun registerUser(user: User): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExposedKidioRepository : KidioRepository {
|
|
||||||
override suspend fun getTasks(
|
|
||||||
subject: String?,
|
|
||||||
age: Int?,
|
|
||||||
limit: Int,
|
|
||||||
): List<Task> =
|
|
||||||
DatabaseFactory.dbQuery {
|
|
||||||
var query = TasksTable.selectAll()
|
|
||||||
if (!subject.isNullOrBlank()) {
|
|
||||||
query = query.where { TasksTable.subject.lowerCase() eq subject.trim().lowercase() }
|
|
||||||
}
|
|
||||||
if (age != null) {
|
|
||||||
query = query.andWhere { (TasksTable.minAge lessEq age) and (TasksTable.maxAge greaterEq age) }
|
|
||||||
}
|
|
||||||
val safeLimit = if (limit <= 0) 10 else limit
|
|
||||||
query.limit(safeLimit).map {
|
|
||||||
Task(
|
|
||||||
id = it[TasksTable.id],
|
|
||||||
subject = it[TasksTable.subject],
|
|
||||||
minAge = it[TasksTable.minAge],
|
|
||||||
maxAge = it[TasksTable.maxAge],
|
|
||||||
question = it[TasksTable.question],
|
|
||||||
options = it[TasksTable.options].split(",").map { opt -> opt.trim() },
|
|
||||||
rewardMinutes = it[TasksTable.rewardMinutes],
|
|
||||||
license = it[TasksTable.license],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getUserById(userId: String): User? =
|
|
||||||
DatabaseFactory.dbQuery {
|
|
||||||
UserTable.selectAll()
|
|
||||||
.where { UserTable.id eq userId }
|
|
||||||
.map {
|
|
||||||
User(
|
|
||||||
id = it[UserTable.id],
|
|
||||||
email = it[UserTable.email],
|
|
||||||
passwordHash = it[UserTable.passwordHash],
|
|
||||||
familyName = it[UserTable.familyName],
|
|
||||||
childAge = it[UserTable.childAge],
|
|
||||||
parentPin = it[UserTable.parentPin],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getAllTasks(): List<Task> = getTasks(limit = 100)
|
|
||||||
|
|
||||||
// NEU: Implementierung für Exposed (PostgreSQL)
|
|
||||||
override suspend fun getExistingTaskIds(externalSource: String): Set<String> =
|
|
||||||
DatabaseFactory.dbQuery {
|
|
||||||
TasksTable
|
|
||||||
.select(TasksTable.id) // Wir brauchen nur die ID-Spalte
|
|
||||||
.where { TasksTable.externalSource eq externalSource }
|
|
||||||
.map { it[TasksTable.id] }
|
|
||||||
.toSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun upsertTask(task: ImportedTask): Boolean =
|
|
||||||
DatabaseFactory.dbQuery {
|
|
||||||
try {
|
|
||||||
val updated =
|
|
||||||
TasksTable.update({ TasksTable.id eq task.id }) {
|
|
||||||
it[externalSource] = task.externalSource
|
|
||||||
it[sourceUrl] = task.sourceUrl
|
|
||||||
it[license] = task.license
|
|
||||||
it[subject] = task.subject
|
|
||||||
it[minAge] = task.minAge
|
|
||||||
it[maxAge] = task.maxAge
|
|
||||||
it[taskType] = task.taskType
|
|
||||||
it[question] = task.question
|
|
||||||
it[options] = task.options.joinToString(",")
|
|
||||||
it[correctAnswer] = task.correctAnswer
|
|
||||||
it[rewardMinutes] = task.rewardMinutes
|
|
||||||
}
|
|
||||||
if (updated == 0) {
|
|
||||||
TasksTable.insert {
|
|
||||||
it[id] = task.id
|
|
||||||
it[externalSource] = task.externalSource
|
|
||||||
it[sourceUrl] = task.sourceUrl
|
|
||||||
it[license] = task.license
|
|
||||||
it[subject] = task.subject
|
|
||||||
it[minAge] = task.minAge
|
|
||||||
it[maxAge] = task.maxAge
|
|
||||||
it[taskType] = task.taskType
|
|
||||||
it[question] = task.question
|
|
||||||
it[options] = task.options.joinToString(",")
|
|
||||||
it[correctAnswer] = task.correctAnswer
|
|
||||||
it[rewardMinutes] = task.rewardMinutes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("FEHLER beim Upsert von Task ${task.id}: ${e.message}")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun registerUser(user: User): Boolean =
|
|
||||||
DatabaseFactory.dbQuery {
|
|
||||||
try {
|
|
||||||
UserTable.insert {
|
|
||||||
it[id] = user.id
|
|
||||||
it[email] = user.email
|
|
||||||
it[passwordHash] = user.passwordHash
|
|
||||||
it[familyName] = user.familyName
|
|
||||||
it[childAge] = user.childAge
|
|
||||||
it[parentPin] = user.parentPin
|
|
||||||
}
|
|
||||||
ScreentimeTable.insert {
|
|
||||||
it[userId] = user.id
|
|
||||||
it[remainingSeconds] = 1800 // Startguthaben: 30 Minuten
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("FEHLER bei Registrierung: ${e.message}")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>? =
|
|
||||||
DatabaseFactory.dbQuery {
|
|
||||||
TasksTable.selectAll()
|
|
||||||
.where { TasksTable.id eq taskId }
|
|
||||||
.map { it[TasksTable.correctAnswer] to it[TasksTable.rewardMinutes] }
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getScreentime(userId: String): Int {
|
|
||||||
val redisKey = "screentime:$userId"
|
|
||||||
|
|
||||||
try {
|
|
||||||
RedisFactory.getResource().use { jedis ->
|
|
||||||
val cachedSeconds = jedis.get(redisKey)
|
|
||||||
if (cachedSeconds != null) {
|
|
||||||
println("DEBUG: Bildschirmzeit für $userId aus REDIS geladen: $cachedSeconds Sek")
|
|
||||||
return cachedSeconds.toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("WARNUNG: Konnte nicht aus Redis lesen (Postgres-Fallback genutzt): ${e.message}")
|
|
||||||
}
|
|
||||||
|
|
||||||
val dbSeconds =
|
|
||||||
DatabaseFactory.dbQuery {
|
|
||||||
ScreentimeTable.selectAll()
|
|
||||||
.where { ScreentimeTable.userId eq userId }
|
|
||||||
.map { it[ScreentimeTable.remainingSeconds] }
|
|
||||||
.singleOrNull() ?: 1800
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
RedisFactory.getResource().use { jedis ->
|
|
||||||
jedis.setex(redisKey, 3600, dbSeconds.toString())
|
|
||||||
println("DEBUG: Bildschirmzeit für $userId in REDIS gecacht.")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("WARNUNG: Konnte Wert nicht in Redis speichern: ${e.message}")
|
|
||||||
}
|
|
||||||
return dbSeconds
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun updateScreentime(
|
|
||||||
userId: String,
|
|
||||||
seconds: Int,
|
|
||||||
) {
|
|
||||||
DatabaseFactory.dbQuery {
|
|
||||||
val rowsUpdated =
|
|
||||||
ScreentimeTable.update({ ScreentimeTable.userId eq userId }) {
|
|
||||||
it[remainingSeconds] = seconds
|
|
||||||
}
|
|
||||||
if (rowsUpdated == 0) {
|
|
||||||
ScreentimeTable.insert {
|
|
||||||
it[ScreentimeTable.userId] = userId
|
|
||||||
it[ScreentimeTable.remainingSeconds] = seconds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val redisKey = "screentime:$userId"
|
|
||||||
try {
|
|
||||||
RedisFactory.getResource().use { jedis ->
|
|
||||||
jedis.setex(redisKey, 3600, seconds.toString())
|
|
||||||
println("DEBUG: Bildschirmzeit für $userId in REDIS aktualisiert auf $seconds Sek")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("WARNUNG: Konnte Redis nicht aktualisieren: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByEmail(email: String): User? =
|
|
||||||
DatabaseFactory.dbQuery {
|
|
||||||
UserTable.selectAll()
|
|
||||||
.where { UserTable.email eq email }
|
|
||||||
.map {
|
|
||||||
User(
|
|
||||||
id = it[UserTable.id],
|
|
||||||
email = it[UserTable.email],
|
|
||||||
passwordHash = it[UserTable.passwordHash],
|
|
||||||
familyName = it[UserTable.familyName],
|
|
||||||
childAge = it[UserTable.childAge],
|
|
||||||
parentPin = it[UserTable.parentPin],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun verifyPassword(
|
|
||||||
password: String,
|
|
||||||
passwordHash: String,
|
|
||||||
): Boolean {
|
|
||||||
return BCrypt.checkpw(password, passwordHash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class InMemoryKidioRepository : KidioRepository {
|
|
||||||
private val tasks =
|
|
||||||
mutableListOf(
|
|
||||||
ImportedTask(
|
|
||||||
id = "t1",
|
|
||||||
externalSource = "manual",
|
|
||||||
subject = "Mathematik",
|
|
||||||
minAge = 6,
|
|
||||||
maxAge = 10,
|
|
||||||
taskType = "MULTIPLE_CHOICE",
|
|
||||||
question = "Was ist 12 + 15?",
|
|
||||||
options = listOf("25", "27", "30", "22"),
|
|
||||||
correctAnswer = "27",
|
|
||||||
rewardMinutes = 10,
|
|
||||||
license = "CC BY-SA 4.0",
|
|
||||||
sourceUrl = "local",
|
|
||||||
),
|
|
||||||
ImportedTask(
|
|
||||||
id = "t2",
|
|
||||||
externalSource = "manual",
|
|
||||||
subject = "Mathematik",
|
|
||||||
minAge = 7,
|
|
||||||
maxAge = 12,
|
|
||||||
taskType = "MULTIPLE_CHOICE",
|
|
||||||
question = "Was ist 8 x 7?",
|
|
||||||
options = listOf("54", "56", "64", "48"),
|
|
||||||
correctAnswer = "56",
|
|
||||||
rewardMinutes = 15,
|
|
||||||
license = "CC BY-SA 4.0",
|
|
||||||
sourceUrl = "local",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
private val screentimes =
|
|
||||||
mutableMapOf(
|
|
||||||
"default_user" to 1800,
|
|
||||||
)
|
|
||||||
private val users = mutableListOf<User>()
|
|
||||||
|
|
||||||
override suspend fun getAllTasks(): List<Task> = getTasks(limit = 100)
|
|
||||||
|
|
||||||
override suspend fun getTasks(
|
|
||||||
subject: String?,
|
|
||||||
age: Int?,
|
|
||||||
limit: Int,
|
|
||||||
): List<Task> {
|
|
||||||
val safeLimit = if (limit <= 0) 10 else limit
|
|
||||||
return tasks.filter { task ->
|
|
||||||
val matchSubject = subject.isNullOrBlank() || task.subject.equals(subject.trim(), ignoreCase = true)
|
|
||||||
val matchAge = age == null || (task.minAge <= age && task.maxAge >= age)
|
|
||||||
matchSubject && matchAge
|
|
||||||
}.take(safeLimit).map {
|
|
||||||
Task(
|
|
||||||
id = it.id,
|
|
||||||
subject = it.subject,
|
|
||||||
minAge = it.minAge,
|
|
||||||
maxAge = it.maxAge,
|
|
||||||
question = it.question,
|
|
||||||
options = it.options,
|
|
||||||
rewardMinutes = it.rewardMinutes,
|
|
||||||
license = it.license,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getUserById(userId: String): User? {
|
|
||||||
return users.find { it.id == userId }
|
|
||||||
}
|
|
||||||
|
|
||||||
// NEU: Implementierung für InMemory (Mock-Daten)
|
|
||||||
override suspend fun getExistingTaskIds(externalSource: String): Set<String> {
|
|
||||||
return tasks
|
|
||||||
.filter { it.externalSource == externalSource }
|
|
||||||
.map { it.id }
|
|
||||||
.toSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun upsertTask(task: ImportedTask): Boolean {
|
|
||||||
tasks.removeIf { it.id == task.id }
|
|
||||||
tasks.add(task)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>? {
|
|
||||||
val task = tasks.find { it.id == taskId } ?: return null
|
|
||||||
return task.correctAnswer to task.rewardMinutes
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getScreentime(userId: String): Int = screentimes[userId] ?: 1800
|
|
||||||
|
|
||||||
override suspend fun updateScreentime(
|
|
||||||
userId: String,
|
|
||||||
seconds: Int,
|
|
||||||
) {
|
|
||||||
screentimes[userId] = seconds
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByEmail(email: String): User? {
|
|
||||||
return users.find { it.email == email }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun verifyPassword(
|
|
||||||
password: String,
|
|
||||||
passwordHash: String,
|
|
||||||
): Boolean {
|
|
||||||
return BCrypt.checkpw(password, passwordHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun registerUser(user: User): Boolean {
|
|
||||||
users.add(user)
|
|
||||||
screentimes[user.id] = 1800
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package com.oliver.kidio.backend.ingestion
|
package com.oliver.kidio.backend.ingestion
|
||||||
|
|
||||||
import com.oliver.kidio.backend.ImportedTask
|
import com.oliver.kidio.backend.data.tables.TasksTable
|
||||||
import com.oliver.kidio.backend.data.database.TasksTable
|
import com.oliver.kidio.backend.domain.models.ImportedTask
|
||||||
import com.oliver.kidio.backend.domain.repository.ExposedKidioRepository
|
import com.oliver.kidio.backend.domain.repository.ExposedKidioRepository
|
||||||
import com.oliver.kidio.backend.domain.repository.KidioRepository
|
import com.oliver.kidio.backend.domain.repository.KidioRepository
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,12 @@
|
||||||
package com.oliver.kidio.backend.plugins
|
package com.oliver.kidio.backend.plugins
|
||||||
|
|
||||||
import com.oliver.kidio.backend.*
|
|
||||||
import com.oliver.kidio.backend.domain.repository.KidioRepository
|
import com.oliver.kidio.backend.domain.repository.KidioRepository
|
||||||
|
import com.oliver.kidio.backend.routing.*
|
||||||
import com.oliver.kidio.backend.security.JwtService
|
import com.oliver.kidio.backend.security.JwtService
|
||||||
import io.ktor.http.HttpStatusCode
|
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.auth.authenticate
|
|
||||||
import io.ktor.server.auth.principal
|
|
||||||
import io.ktor.server.plugins.openapi.*
|
import io.ktor.server.plugins.openapi.*
|
||||||
import io.ktor.server.request.*
|
|
||||||
import io.ktor.server.response.*
|
import io.ktor.server.response.*
|
||||||
import io.ktor.server.routing.*
|
import io.ktor.server.routing.*
|
||||||
import org.mindrot.jbcrypt.BCrypt
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
fun isValidPassword(password: String): Boolean {
|
|
||||||
if (password.length < 8) return false
|
|
||||||
|
|
||||||
val hasUppercase = password.any { it.isUpperCase() }
|
|
||||||
val hasEmoji =
|
|
||||||
password.any { char ->
|
|
||||||
val type = Character.getType(char).toByte()
|
|
||||||
type == Character.SURROGATE ||
|
|
||||||
type == Character.OTHER_SYMBOL ||
|
|
||||||
char.code in 0x1F600..0x1F64F ||
|
|
||||||
char.code in 0x1F300..0x1F5FF ||
|
|
||||||
char.code in 0x1F680..0x1F6FF ||
|
|
||||||
char.code in 0x2600..0x26FF
|
|
||||||
}
|
|
||||||
|
|
||||||
val hasSpecialChar = password.any { !it.isLetterOrDigit() && !hasEmoji }
|
|
||||||
|
|
||||||
return hasUppercase && hasSpecialChar && !hasEmoji
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Application.configureRouting(
|
fun Application.configureRouting(
|
||||||
repository: KidioRepository,
|
repository: KidioRepository,
|
||||||
|
|
@ -41,195 +15,13 @@ fun Application.configureRouting(
|
||||||
routing {
|
routing {
|
||||||
openAPI(path = "openapi")
|
openAPI(path = "openapi")
|
||||||
|
|
||||||
post("/api/v1/auth/register") {
|
|
||||||
val registerRequest = call.receive<RegisterRequest>()
|
|
||||||
if (registerRequest.email.isBlank() || registerRequest.passwordPlain.isBlank()) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, "Bitte füllen Sie alle Felder aus")
|
|
||||||
return@post
|
|
||||||
}
|
|
||||||
if (!isValidPassword(registerRequest.passwordPlain)) {
|
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
"Das Passwort muss mindestens 8 Zeichen lang sein, " +
|
|
||||||
"mindestens einen Großbuchstaben und ein Sonderzeichen enthalten.",
|
|
||||||
)
|
|
||||||
return@post
|
|
||||||
}
|
|
||||||
if (registerRequest.parentPin.length != 4) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, "Die Eltern-PIN muss genau 4 Ziffern lang sein")
|
|
||||||
return@post
|
|
||||||
}
|
|
||||||
val existingUser = repository.findByEmail(registerRequest.email)
|
|
||||||
if (existingUser != null) {
|
|
||||||
call.respond(HttpStatusCode.Conflict, "Ein Benutzer mit dieser E-Mail-Adresse existiert bereits")
|
|
||||||
return@post
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backend generiert die eindeutige User-ID
|
|
||||||
val userId = UUID.randomUUID().toString()
|
|
||||||
val passwordHash = BCrypt.hashpw(registerRequest.passwordPlain, BCrypt.gensalt())
|
|
||||||
|
|
||||||
// datenbank insert hinzufügen
|
|
||||||
repository.registerUser(
|
|
||||||
User(
|
|
||||||
userId,
|
|
||||||
registerRequest.email,
|
|
||||||
passwordHash,
|
|
||||||
registerRequest.familyName,
|
|
||||||
registerRequest.childAge,
|
|
||||||
registerRequest.parentPin,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
val token = jwtService.generateToken(userId)
|
|
||||||
|
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.Created,
|
|
||||||
mapOf(
|
|
||||||
"token" to token,
|
|
||||||
"userId" to userId,
|
|
||||||
"message" to "Registrierung erfolgreich",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// Health-Check
|
|
||||||
get("/api/v1/health") {
|
get("/api/v1/health") {
|
||||||
call.respond(mapOf("status" to "OK", "message" to "Kidio Backend läuft!"))
|
call.respond(mapOf("status" to "OK", "message" to "Kidio Backend läuft!"))
|
||||||
}
|
}
|
||||||
|
|
||||||
post("/api/v1/auth/login") {
|
authRoutes(repository, jwtService)
|
||||||
val loginRequest = call.receive<LoginRequest>()
|
taskRoutes(repository)
|
||||||
if (loginRequest.email.isBlank() || loginRequest.passwordPlain.isBlank()) {
|
screentimeRoutes(repository)
|
||||||
call.respond(HttpStatusCode.BadRequest, "Bitte füllen Sie alle Felder aus")
|
parentRoutes(repository)
|
||||||
return@post
|
|
||||||
}
|
|
||||||
val getUser = repository.findByEmail(loginRequest.email)
|
|
||||||
if (getUser == null || !repository.verifyPassword(loginRequest.passwordPlain, getUser.passwordHash)) {
|
|
||||||
call.respond(HttpStatusCode.Unauthorized, "Ungültige Anmeldedaten")
|
|
||||||
return@post
|
|
||||||
}
|
|
||||||
val token = jwtService.generateToken(getUser.id)
|
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.OK,
|
|
||||||
AuthResponse(
|
|
||||||
token,
|
|
||||||
userId = getUser.id,
|
|
||||||
familyName = getUser.familyName,
|
|
||||||
childAge = getUser.childAge,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return@post
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticate("auth-jwt") {
|
|
||||||
// 1. Alle verfügbaren Aufgaben abrufen (mit optionaler Filterung nach Fach und Alter)
|
|
||||||
get("/api/v1/tasks") {
|
|
||||||
val principal = call.principal<UserPrincipal>()
|
|
||||||
val userId = principal?.userId
|
|
||||||
|
|
||||||
val subject = call.request.queryParameters["subject"]
|
|
||||||
|
|
||||||
// Expliziter Query-Parameter 'age' ODER Fallback auf das Alter des Users
|
|
||||||
val queryAge = call.request.queryParameters["age"]?.toIntOrNull()
|
|
||||||
val userAge = userId?.let { repository.getUserById(it)?.childAge }
|
|
||||||
val effectiveAge = queryAge ?: userAge
|
|
||||||
|
|
||||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 10
|
|
||||||
|
|
||||||
val tasks = repository.getTasks(subject = subject, age = effectiveAge, limit = limit)
|
|
||||||
call.respond(tasks)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Antwort auf eine Aufgabe überprüfen
|
|
||||||
post("/api/v1/tasks/verify") {
|
|
||||||
val request = call.receive<TaskVerifyRequest>()
|
|
||||||
val principal = call.principal<UserPrincipal>()
|
|
||||||
val userId = principal?.userId
|
|
||||||
if (userId == null) {
|
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.Unauthorized,
|
|
||||||
mapOf("error" to "Ungültiges oder abgelaufenes Token"),
|
|
||||||
)
|
|
||||||
return@post
|
|
||||||
}
|
|
||||||
val taskInfo = repository.getTaskAnswerAndReward(request.taskId)
|
|
||||||
if (taskInfo == null) {
|
|
||||||
call.respond(
|
|
||||||
TaskVerifyResponse(
|
|
||||||
isCorrect = false,
|
|
||||||
earnedMinutes = 0,
|
|
||||||
message = "Aufgabe nicht gefunden!",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return@post
|
|
||||||
}
|
|
||||||
|
|
||||||
val (correctAnswer, reward) = taskInfo
|
|
||||||
val isCorrect = request.selectedAnswer.trim() == correctAnswer.trim()
|
|
||||||
|
|
||||||
if (isCorrect) {
|
|
||||||
val currentScreentime = repository.getScreentime(userId)
|
|
||||||
val newSeconds = currentScreentime + (reward * 60)
|
|
||||||
repository.updateScreentime(userId, newSeconds)
|
|
||||||
|
|
||||||
call.respond(
|
|
||||||
TaskVerifyResponse(
|
|
||||||
isCorrect = true,
|
|
||||||
earnedMinutes = reward,
|
|
||||||
message = "Super gemacht! Du hast $reward Minuten gewonnen.",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
call.respond(
|
|
||||||
TaskVerifyResponse(
|
|
||||||
isCorrect = false,
|
|
||||||
earnedMinutes = 0,
|
|
||||||
message = "Schade, das war leider falsch. Versuche es nochmal!",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get("/api/v1/screentime") {
|
|
||||||
val principal = call.principal<UserPrincipal>()
|
|
||||||
val userid =
|
|
||||||
principal?.userId
|
|
||||||
?: run {
|
|
||||||
call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Ungültiger Token"))
|
|
||||||
return@get
|
|
||||||
}
|
|
||||||
val remainingSeconds = repository.getScreentime(userid)
|
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.OK,
|
|
||||||
SyncResponse(
|
|
||||||
remainingSeconds = remainingSeconds,
|
|
||||||
isBlocked = remainingSeconds <= 0,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Bildschirmzeit synchronisieren
|
|
||||||
post("/api/v1/screentime/sync") {
|
|
||||||
val request = call.receive<SyncRequest>()
|
|
||||||
|
|
||||||
// Wir holen die echte, fälschungssichere userId aus dem Login-Token:
|
|
||||||
val principal = call.principal<UserPrincipal>()
|
|
||||||
val userId =
|
|
||||||
principal?.userId ?: request.userId // Fallback auf request.userId, falls token noch fehlt
|
|
||||||
|
|
||||||
val currentSeconds = repository.getScreentime(userId)
|
|
||||||
var newSeconds = currentSeconds - request.usedSecondsSinceLastSync
|
|
||||||
if (newSeconds < 0) newSeconds = 0
|
|
||||||
|
|
||||||
repository.updateScreentime(userId, newSeconds)
|
|
||||||
|
|
||||||
call.respond(
|
|
||||||
SyncResponse(
|
|
||||||
remainingSeconds = newSeconds,
|
|
||||||
isBlocked = newSeconds <= 0,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
package com.oliver.kidio.backend.routing
|
||||||
|
|
||||||
|
import com.oliver.kidio.backend.domain.models.AuthResponse
|
||||||
|
import com.oliver.kidio.backend.domain.models.LoginRequest
|
||||||
|
import com.oliver.kidio.backend.domain.models.RegisterRequest
|
||||||
|
import com.oliver.kidio.backend.domain.models.User
|
||||||
|
import com.oliver.kidio.backend.domain.repository.KidioRepository
|
||||||
|
import com.oliver.kidio.backend.security.JwtService
|
||||||
|
import com.oliver.kidio.backend.security.isValidPassword
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.server.request.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
import org.mindrot.jbcrypt.BCrypt
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
fun Route.authRoutes(
|
||||||
|
repository: KidioRepository,
|
||||||
|
jwtService: JwtService,
|
||||||
|
) {
|
||||||
|
post("/api/v1/auth/register") {
|
||||||
|
val registerRequest = call.receive<RegisterRequest>()
|
||||||
|
if (registerRequest.email.isBlank() || registerRequest.passwordPlain.isBlank()) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, "Bitte füllen Sie alle Felder aus")
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
if (!isValidPassword(registerRequest.passwordPlain)) {
|
||||||
|
call.respond(
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
"Das Passwort muss mindestens 8 Zeichen lang sein, " +
|
||||||
|
"mindestens einen Großbuchstaben und ein Sonderzeichen enthalten.",
|
||||||
|
)
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
if (registerRequest.parentPin.length != 4) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, "Die Eltern-PIN muss genau 4 Ziffern lang sein")
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
val existingUser = repository.findByEmail(registerRequest.email)
|
||||||
|
if (existingUser != null) {
|
||||||
|
call.respond(HttpStatusCode.Conflict, "Ein Benutzer mit dieser E-Mail-Adresse existiert bereits")
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
|
||||||
|
val userId = UUID.randomUUID().toString()
|
||||||
|
val passwordHash = BCrypt.hashpw(registerRequest.passwordPlain, BCrypt.gensalt())
|
||||||
|
|
||||||
|
repository.registerUser(
|
||||||
|
User(
|
||||||
|
userId,
|
||||||
|
registerRequest.email,
|
||||||
|
passwordHash,
|
||||||
|
registerRequest.familyName,
|
||||||
|
registerRequest.childAge,
|
||||||
|
registerRequest.parentPin,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val token = jwtService.generateToken(userId)
|
||||||
|
|
||||||
|
call.respond(
|
||||||
|
HttpStatusCode.Created,
|
||||||
|
mapOf(
|
||||||
|
"token" to token,
|
||||||
|
"userId" to userId,
|
||||||
|
"message" to "Registrierung erfolgreich",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
post("/api/v1/auth/login") {
|
||||||
|
val loginRequest = call.receive<LoginRequest>()
|
||||||
|
if (loginRequest.email.isBlank() || loginRequest.passwordPlain.isBlank()) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, "Bitte füllen Sie alle Felder aus")
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
val getUser = repository.findByEmail(loginRequest.email)
|
||||||
|
if (getUser == null || !repository.verifyPassword(loginRequest.passwordPlain, getUser.passwordHash)) {
|
||||||
|
call.respond(HttpStatusCode.Unauthorized, "Ungültige Anmeldedaten")
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
val token = jwtService.generateToken(getUser.id)
|
||||||
|
call.respond(
|
||||||
|
HttpStatusCode.OK,
|
||||||
|
AuthResponse(
|
||||||
|
token,
|
||||||
|
userId = getUser.id,
|
||||||
|
familyName = getUser.familyName,
|
||||||
|
childAge = getUser.childAge,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package com.oliver.kidio.backend.routing
|
||||||
|
|
||||||
|
import com.oliver.kidio.backend.domain.models.VerifyPinRequest
|
||||||
|
import com.oliver.kidio.backend.domain.models.VerifyPinResponse
|
||||||
|
import com.oliver.kidio.backend.domain.repository.KidioRepository
|
||||||
|
import com.oliver.kidio.backend.plugins.UserPrincipal
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.server.auth.*
|
||||||
|
import io.ktor.server.request.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
|
||||||
|
fun Route.parentRoutes(repository: KidioRepository) {
|
||||||
|
authenticate("auth-jwt") {
|
||||||
|
post("/api/v1/parent/verify-pin") {
|
||||||
|
val request = call.receive<VerifyPinRequest>()
|
||||||
|
val principal = call.principal<UserPrincipal>()
|
||||||
|
val userId = principal?.userId
|
||||||
|
|
||||||
|
if (userId == null) {
|
||||||
|
call.respond(
|
||||||
|
HttpStatusCode.Unauthorized,
|
||||||
|
mapOf("error" to "Ungültiges oder abgelaufenes Token"),
|
||||||
|
)
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.pin.length != 4 || !request.pin.all { it.isDigit() }) {
|
||||||
|
call.respond(
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
VerifyPinResponse(success = false, message = "Die PIN muss aus genau 4 Ziffern bestehen."),
|
||||||
|
)
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
|
||||||
|
val user = repository.findById(userId)
|
||||||
|
if (user == null) {
|
||||||
|
call.respond(
|
||||||
|
HttpStatusCode.NotFound,
|
||||||
|
VerifyPinResponse(success = false, message = "Benutzer nicht gefunden."),
|
||||||
|
)
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.parentPin == request.pin) {
|
||||||
|
call.respond(
|
||||||
|
HttpStatusCode.OK,
|
||||||
|
VerifyPinResponse(success = true, message = "PIN korrekt. Zugriff gewährt."),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
call.respond(
|
||||||
|
HttpStatusCode.Unauthorized,
|
||||||
|
VerifyPinResponse(success = false, message = "Falsche PIN."),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package com.oliver.kidio.backend.routing
|
||||||
|
|
||||||
|
import com.oliver.kidio.backend.domain.models.SyncRequest
|
||||||
|
import com.oliver.kidio.backend.domain.models.SyncResponse
|
||||||
|
import com.oliver.kidio.backend.domain.repository.KidioRepository
|
||||||
|
import com.oliver.kidio.backend.plugins.UserPrincipal
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.server.auth.*
|
||||||
|
import io.ktor.server.request.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
|
||||||
|
fun Route.screentimeRoutes(repository: KidioRepository) {
|
||||||
|
authenticate("auth-jwt") {
|
||||||
|
get("/api/v1/screentime") {
|
||||||
|
val principal = call.principal<UserPrincipal>()
|
||||||
|
val userid =
|
||||||
|
principal?.userId
|
||||||
|
?: run {
|
||||||
|
call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Ungültiger Token"))
|
||||||
|
return@get
|
||||||
|
}
|
||||||
|
val remainingSeconds = repository.getScreentime(userid)
|
||||||
|
call.respond(
|
||||||
|
HttpStatusCode.OK,
|
||||||
|
SyncResponse(
|
||||||
|
remainingSeconds = remainingSeconds,
|
||||||
|
isBlocked = remainingSeconds <= 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
post("/api/v1/screentime/sync") {
|
||||||
|
val request = call.receive<SyncRequest>()
|
||||||
|
|
||||||
|
val principal = call.principal<UserPrincipal>()
|
||||||
|
val userId = principal?.userId ?: request.userId
|
||||||
|
|
||||||
|
val currentSeconds = repository.getScreentime(userId)
|
||||||
|
var newSeconds = currentSeconds - request.usedSecondsSinceLastSync
|
||||||
|
if (newSeconds < 0) newSeconds = 0
|
||||||
|
|
||||||
|
repository.updateScreentime(userId, newSeconds)
|
||||||
|
|
||||||
|
call.respond(
|
||||||
|
SyncResponse(
|
||||||
|
remainingSeconds = newSeconds,
|
||||||
|
isBlocked = newSeconds <= 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
package com.oliver.kidio.backend.routing
|
||||||
|
|
||||||
|
import com.oliver.kidio.backend.domain.models.TaskVerifyRequest
|
||||||
|
import com.oliver.kidio.backend.domain.models.TaskVerifyResponse
|
||||||
|
import com.oliver.kidio.backend.domain.repository.KidioRepository
|
||||||
|
import com.oliver.kidio.backend.plugins.UserPrincipal
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.server.auth.*
|
||||||
|
import io.ktor.server.request.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
|
||||||
|
fun Route.taskRoutes(repository: KidioRepository) {
|
||||||
|
authenticate("auth-jwt") {
|
||||||
|
get("/api/v1/tasks") {
|
||||||
|
val principal = call.principal<UserPrincipal>()
|
||||||
|
val userId = principal?.userId
|
||||||
|
|
||||||
|
val subject = call.request.queryParameters["subject"]
|
||||||
|
|
||||||
|
val queryAge = call.request.queryParameters["age"]?.toIntOrNull()
|
||||||
|
val userAge = userId?.let { repository.getUserById(it)?.childAge }
|
||||||
|
val effectiveAge = queryAge ?: userAge
|
||||||
|
|
||||||
|
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 10
|
||||||
|
|
||||||
|
val tasks = repository.getTasks(subject = subject, age = effectiveAge, limit = limit)
|
||||||
|
call.respond(tasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
post("/api/v1/tasks/verify") {
|
||||||
|
val request = call.receive<TaskVerifyRequest>()
|
||||||
|
val principal = call.principal<UserPrincipal>()
|
||||||
|
val userId = principal?.userId
|
||||||
|
if (userId == null) {
|
||||||
|
call.respond(
|
||||||
|
HttpStatusCode.Unauthorized,
|
||||||
|
mapOf("error" to "Ungültiges oder abgelaufenes Token"),
|
||||||
|
)
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
val taskInfo = repository.getTaskAnswerAndReward(request.taskId)
|
||||||
|
if (taskInfo == null) {
|
||||||
|
call.respond(
|
||||||
|
TaskVerifyResponse(
|
||||||
|
isCorrect = false,
|
||||||
|
earnedMinutes = 0,
|
||||||
|
message = "Aufgabe nicht gefunden!",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
|
||||||
|
val (correctAnswer, reward) = taskInfo
|
||||||
|
val isCorrect = request.selectedAnswer.trim() == correctAnswer.trim()
|
||||||
|
|
||||||
|
if (isCorrect) {
|
||||||
|
val currentScreentime = repository.getScreentime(userId)
|
||||||
|
val newSeconds = currentScreentime + (reward * 60)
|
||||||
|
repository.updateScreentime(userId, newSeconds)
|
||||||
|
|
||||||
|
call.respond(
|
||||||
|
TaskVerifyResponse(
|
||||||
|
isCorrect = true,
|
||||||
|
earnedMinutes = reward,
|
||||||
|
message = "Super gemacht! Du hast $reward Minuten gewonnen.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
call.respond(
|
||||||
|
TaskVerifyResponse(
|
||||||
|
isCorrect = false,
|
||||||
|
earnedMinutes = 0,
|
||||||
|
message = "Schade, das war leider falsch. Versuche es nochmal!",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.oliver.kidio.backend.security
|
||||||
|
|
||||||
|
fun isValidPassword(password: String): Boolean {
|
||||||
|
if (password.length < 8) return false
|
||||||
|
|
||||||
|
val hasUppercase = password.any { it.isUpperCase() }
|
||||||
|
val hasEmoji =
|
||||||
|
password.any { char ->
|
||||||
|
val type = Character.getType(char).toByte()
|
||||||
|
type == Character.SURROGATE ||
|
||||||
|
type == Character.OTHER_SYMBOL ||
|
||||||
|
char.code in 0x1F600..0x1F64F ||
|
||||||
|
char.code in 0x1F300..0x1F5FF ||
|
||||||
|
char.code in 0x1F680..0x1F6FF ||
|
||||||
|
char.code in 0x2600..0x26FF
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasSpecialChar = password.any { !it.isLetterOrDigit() && !hasEmoji }
|
||||||
|
|
||||||
|
return hasUppercase && hasSpecialChar && !hasEmoji
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.oliver.kidio.backend
|
package com.oliver.kidio.backend.com.oliver.kidio.backend.ingestion
|
||||||
|
|
||||||
import com.oliver.kidio.backend.domain.repository.InMemoryKidioRepository
|
import com.oliver.kidio.backend.domain.repository.InMemoryKidioRepository
|
||||||
import com.oliver.kidio.backend.ingestion.H5PIngestionCli
|
import com.oliver.kidio.backend.ingestion.H5PIngestionCli
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
package com.oliver.kidio.backend.routing
|
||||||
|
|
||||||
|
import com.oliver.kidio.backend.domain.models.AuthResponse
|
||||||
|
import com.oliver.kidio.backend.domain.repository.InMemoryKidioRepository
|
||||||
|
import com.oliver.kidio.backend.plugins.configureRouting
|
||||||
|
import com.oliver.kidio.backend.plugins.configureSecurity
|
||||||
|
import com.oliver.kidio.backend.security.JwtService
|
||||||
|
import io.ktor.client.request.header
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import io.ktor.client.statement.bodyAsText
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.HttpHeaders
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
|
import io.ktor.server.application.install
|
||||||
|
import io.ktor.server.testing.testApplication
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation
|
||||||
|
|
||||||
|
class AuthRoutesTest {
|
||||||
|
@Test
|
||||||
|
fun `test registration and login flow`() =
|
||||||
|
testApplication {
|
||||||
|
val jwtService = JwtService()
|
||||||
|
val repository = InMemoryKidioRepository()
|
||||||
|
|
||||||
|
application {
|
||||||
|
install(ServerContentNegotiation) {
|
||||||
|
json()
|
||||||
|
}
|
||||||
|
configureSecurity(jwtService)
|
||||||
|
configureRouting(repository, jwtService)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Registrierung
|
||||||
|
val registerResponse =
|
||||||
|
client.post("/api/v1/auth/register") {
|
||||||
|
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
setBody(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"email": "test@family.de",
|
||||||
|
"passwordPlain": "StrengGeheim1!",
|
||||||
|
"familyName": "Familie Schmidt",
|
||||||
|
"childAge": 8,
|
||||||
|
"parentPin": "4321"
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.Created, registerResponse.status)
|
||||||
|
assertTrue(registerResponse.bodyAsText().contains("token"))
|
||||||
|
|
||||||
|
// 2. Login mit denselben Daten
|
||||||
|
val loginResponse =
|
||||||
|
client.post("/api/v1/auth/login") {
|
||||||
|
header(
|
||||||
|
HttpHeaders.ContentType,
|
||||||
|
ContentType.Application.Json.toString(),
|
||||||
|
)
|
||||||
|
setBody(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"email": "test@family.de",
|
||||||
|
"passwordPlain": "StrengGeheim1!"
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.OK, loginResponse.status)
|
||||||
|
val loginJson = Json.decodeFromString<AuthResponse>(loginResponse.bodyAsText())
|
||||||
|
assertNotNull(loginJson.token)
|
||||||
|
assertEquals("Familie Schmidt", loginJson.familyName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test registration rejects invalid passwords`() =
|
||||||
|
testApplication {
|
||||||
|
val jwtService = JwtService()
|
||||||
|
val repository = InMemoryKidioRepository()
|
||||||
|
|
||||||
|
application {
|
||||||
|
install(ServerContentNegotiation) {
|
||||||
|
json()
|
||||||
|
}
|
||||||
|
configureSecurity(jwtService)
|
||||||
|
configureRouting(repository, jwtService)
|
||||||
|
}
|
||||||
|
|
||||||
|
val invalidPasswords =
|
||||||
|
listOf(
|
||||||
|
"Kurz1!",
|
||||||
|
"keingrossbuch1!",
|
||||||
|
"KeinSonder12",
|
||||||
|
"Passwort123!😊",
|
||||||
|
)
|
||||||
|
|
||||||
|
for (password in invalidPasswords) {
|
||||||
|
val response =
|
||||||
|
client.post("/api/v1/auth/register") {
|
||||||
|
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
setBody(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"email": "test_${System.currentTimeMillis()}@family.de",
|
||||||
|
"passwordPlain": "$password",
|
||||||
|
"familyName": "Familie Schmidt",
|
||||||
|
"childAge": 8,
|
||||||
|
"parentPin": "4321"
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertEquals(
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
response.status,
|
||||||
|
"Passwort '$password' hätte abgelehnt werden müssen!",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.oliver.kidio.backend.routing
|
||||||
|
|
||||||
|
import com.oliver.kidio.backend.domain.repository.InMemoryKidioRepository
|
||||||
|
import com.oliver.kidio.backend.plugins.configureRouting
|
||||||
|
import com.oliver.kidio.backend.plugins.configureSecurity
|
||||||
|
import com.oliver.kidio.backend.security.JwtService
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.statement.bodyAsText
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
|
import io.ktor.server.application.install
|
||||||
|
import io.ktor.server.testing.testApplication
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation
|
||||||
|
|
||||||
|
class HealthRouteTest {
|
||||||
|
@Test
|
||||||
|
fun `test health endpoint`() =
|
||||||
|
testApplication {
|
||||||
|
val jwtService = JwtService()
|
||||||
|
val repository = InMemoryKidioRepository()
|
||||||
|
|
||||||
|
application {
|
||||||
|
install(ServerContentNegotiation) {
|
||||||
|
json()
|
||||||
|
}
|
||||||
|
configureSecurity(jwtService)
|
||||||
|
configureRouting(repository, jwtService)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = client.get("/api/v1/health")
|
||||||
|
assertEquals(HttpStatusCode.OK, response.status)
|
||||||
|
assertTrue(response.bodyAsText().contains("Kidio Backend läuft!"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
package com.oliver.kidio.backend.routing
|
||||||
|
|
||||||
|
import com.oliver.kidio.backend.domain.models.SyncResponse
|
||||||
|
import com.oliver.kidio.backend.domain.repository.InMemoryKidioRepository
|
||||||
|
import com.oliver.kidio.backend.plugins.configureRouting
|
||||||
|
import com.oliver.kidio.backend.plugins.configureSecurity
|
||||||
|
import com.oliver.kidio.backend.security.JwtService
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.request.header
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import io.ktor.client.statement.bodyAsText
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.HttpHeaders
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
|
import io.ktor.server.application.install
|
||||||
|
import io.ktor.server.testing.testApplication
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation
|
||||||
|
|
||||||
|
class ScreentimeRoutesTest {
|
||||||
|
@Test
|
||||||
|
fun `test get screentime returns current status`() =
|
||||||
|
testApplication {
|
||||||
|
val jwtService = JwtService()
|
||||||
|
val repository = InMemoryKidioRepository()
|
||||||
|
|
||||||
|
application {
|
||||||
|
install(ServerContentNegotiation) { json() }
|
||||||
|
configureSecurity(jwtService)
|
||||||
|
configureRouting(repository, jwtService)
|
||||||
|
}
|
||||||
|
|
||||||
|
val token = jwtService.generateToken("default_user")
|
||||||
|
|
||||||
|
// Ohne Token -> 401 Unauthorized
|
||||||
|
val unauthResponse = client.get("/api/v1/screentime")
|
||||||
|
assertEquals(HttpStatusCode.Unauthorized, unauthResponse.status)
|
||||||
|
|
||||||
|
// Mit Token -> 200 OK
|
||||||
|
val response =
|
||||||
|
client.get("/api/v1/screentime") {
|
||||||
|
header(HttpHeaders.Authorization, "Bearer $token")
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.OK, response.status)
|
||||||
|
val json = Json.decodeFromString<SyncResponse>(response.bodyAsText())
|
||||||
|
assertEquals(1800, json.remainingSeconds)
|
||||||
|
assertFalse(json.isBlocked)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test screentime sync deducts time repeatedly`() =
|
||||||
|
testApplication {
|
||||||
|
val jwtService = JwtService()
|
||||||
|
val repository = InMemoryKidioRepository()
|
||||||
|
|
||||||
|
application {
|
||||||
|
install(ServerContentNegotiation) {
|
||||||
|
json()
|
||||||
|
}
|
||||||
|
configureSecurity(jwtService)
|
||||||
|
configureRouting(repository, jwtService)
|
||||||
|
}
|
||||||
|
|
||||||
|
val token = jwtService.generateToken("default_user")
|
||||||
|
|
||||||
|
// 1. Sync: 200 Sekunden abziehen (1800 -> 1600)
|
||||||
|
val syncResponse1 =
|
||||||
|
client.post("/api/v1/screentime/sync") {
|
||||||
|
header(HttpHeaders.Authorization, "Bearer $token")
|
||||||
|
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
setBody("""{"userId":"default_user","usedSecondsSinceLastSync":200}""")
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.OK, syncResponse1.status)
|
||||||
|
val syncJson1 = Json.decodeFromString<SyncResponse>(syncResponse1.bodyAsText())
|
||||||
|
assertEquals(1600, syncJson1.remainingSeconds)
|
||||||
|
assertFalse(syncJson1.isBlocked)
|
||||||
|
|
||||||
|
// 2. Erneuter Sync: Weitere 300 Sekunden abziehen (1600 -> 1300)
|
||||||
|
val syncResponse2 =
|
||||||
|
client.post("/api/v1/screentime/sync") {
|
||||||
|
header(HttpHeaders.Authorization, "Bearer $token")
|
||||||
|
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
setBody("""{"userId":"default_user","usedSecondsSinceLastSync":300}""")
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.OK, syncResponse2.status)
|
||||||
|
val syncJson2 = Json.decodeFromString<SyncResponse>(syncResponse2.bodyAsText())
|
||||||
|
assertEquals(1300, syncJson2.remainingSeconds)
|
||||||
|
assertFalse(syncJson2.isBlocked)
|
||||||
|
|
||||||
|
// 3. Sync bis zur Sperre (weitere 1500 Sekunden abziehen -> 0)
|
||||||
|
val syncResponse3 =
|
||||||
|
client.post("/api/v1/screentime/sync") {
|
||||||
|
header(HttpHeaders.Authorization, "Bearer $token")
|
||||||
|
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
setBody("""{"userId":"default_user","usedSecondsSinceLastSync":1500}""")
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.OK, syncResponse3.status)
|
||||||
|
val syncJson3 = Json.decodeFromString<SyncResponse>(syncResponse3.bodyAsText())
|
||||||
|
assertEquals(0, syncJson3.remainingSeconds)
|
||||||
|
assertTrue(syncJson3.isBlocked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
package com.oliver.kidio.backend
|
package com.oliver.kidio.backend.com.oliver.kidio.backend.routing
|
||||||
|
|
||||||
|
import com.oliver.kidio.backend.domain.models.AuthResponse
|
||||||
|
import com.oliver.kidio.backend.domain.models.SyncResponse
|
||||||
|
import com.oliver.kidio.backend.domain.models.Task
|
||||||
|
import com.oliver.kidio.backend.domain.models.TaskVerifyResponse
|
||||||
import com.oliver.kidio.backend.domain.repository.InMemoryKidioRepository
|
import com.oliver.kidio.backend.domain.repository.InMemoryKidioRepository
|
||||||
import com.oliver.kidio.backend.plugins.configureRouting
|
import com.oliver.kidio.backend.plugins.configureRouting
|
||||||
import com.oliver.kidio.backend.plugins.configureSecurity
|
import com.oliver.kidio.backend.plugins.configureSecurity
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
package com.oliver.kidio.backend.routing
|
||||||
|
|
||||||
|
import com.oliver.kidio.backend.domain.models.Task
|
||||||
|
import com.oliver.kidio.backend.domain.models.TaskVerifyResponse
|
||||||
|
import com.oliver.kidio.backend.domain.repository.InMemoryKidioRepository
|
||||||
|
import com.oliver.kidio.backend.plugins.configureRouting
|
||||||
|
import com.oliver.kidio.backend.plugins.configureSecurity
|
||||||
|
import com.oliver.kidio.backend.security.JwtService
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.request.header
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
|
import io.ktor.client.statement.bodyAsText
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.HttpHeaders
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
|
import io.ktor.server.application.install
|
||||||
|
import io.ktor.server.testing.testApplication
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation
|
||||||
|
|
||||||
|
class TaskRoutesTest {
|
||||||
|
@Test
|
||||||
|
fun `test protected endpoints reject unauthorized requests`() =
|
||||||
|
testApplication {
|
||||||
|
val jwtService = JwtService()
|
||||||
|
val repository = InMemoryKidioRepository()
|
||||||
|
|
||||||
|
application {
|
||||||
|
install(ServerContentNegotiation) {
|
||||||
|
json()
|
||||||
|
}
|
||||||
|
configureSecurity(jwtService)
|
||||||
|
configureRouting(repository, jwtService)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ohne Token muss 401 Unauthorized kommen
|
||||||
|
val tasksResponse = client.get("/api/v1/tasks")
|
||||||
|
assertEquals(HttpStatusCode.Unauthorized, tasksResponse.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test tasks endpoint and verify with jwt`() =
|
||||||
|
testApplication {
|
||||||
|
val jwtService = JwtService()
|
||||||
|
val repository = InMemoryKidioRepository()
|
||||||
|
|
||||||
|
application {
|
||||||
|
install(ServerContentNegotiation) {
|
||||||
|
json()
|
||||||
|
}
|
||||||
|
configureSecurity(jwtService)
|
||||||
|
configureRouting(repository, jwtService)
|
||||||
|
}
|
||||||
|
|
||||||
|
val token = jwtService.generateToken("default_user")
|
||||||
|
|
||||||
|
// 1. Get Tasks mit Token
|
||||||
|
val tasksResponse =
|
||||||
|
client.get("/api/v1/tasks") {
|
||||||
|
header(HttpHeaders.Authorization, "Bearer $token")
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.OK, tasksResponse.status)
|
||||||
|
val tasksJson = Json.decodeFromString<List<Task>>(tasksResponse.bodyAsText())
|
||||||
|
assertEquals(2, tasksJson.size)
|
||||||
|
assertEquals("t1", tasksJson[0].id)
|
||||||
|
assertEquals("Was ist 12 + 15?", tasksJson[0].question)
|
||||||
|
|
||||||
|
// 2. Filter nach Fach & Alter
|
||||||
|
val filteredResponse =
|
||||||
|
client.get("/api/v1/tasks?subject=Mathematik&age=8") {
|
||||||
|
header(HttpHeaders.Authorization, "Bearer $token")
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.OK, filteredResponse.status)
|
||||||
|
val filteredJson = Json.decodeFromString<List<Task>>(filteredResponse.bodyAsText())
|
||||||
|
assertEquals(2, filteredJson.size)
|
||||||
|
|
||||||
|
// 3. Filter mit Alter außerhalb des Bereichs
|
||||||
|
val age15Response =
|
||||||
|
client.get("/api/v1/tasks?subject=Mathematik&age=15") {
|
||||||
|
header(HttpHeaders.Authorization, "Bearer $token")
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.OK, age15Response.status)
|
||||||
|
val age15Json = Json.decodeFromString<List<Task>>(age15Response.bodyAsText())
|
||||||
|
assertEquals(0, age15Json.size)
|
||||||
|
|
||||||
|
// 4. Antwort enthält NIEMALS correctAnswer
|
||||||
|
assertFalse(tasksResponse.bodyAsText().contains("correctAnswer"))
|
||||||
|
assertFalse(tasksResponse.bodyAsText().contains("correct_answer"))
|
||||||
|
|
||||||
|
// 5. Verify Task correct answer
|
||||||
|
val verifyResponse =
|
||||||
|
client.post("/api/v1/tasks/verify") {
|
||||||
|
header(HttpHeaders.Authorization, "Bearer $token")
|
||||||
|
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
setBody("""{"taskId":"t1","selectedAnswer":"27"}""")
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.OK, verifyResponse.status)
|
||||||
|
val verifyJson = Json.decodeFromString<TaskVerifyResponse>(verifyResponse.bodyAsText())
|
||||||
|
assertTrue(verifyJson.isCorrect)
|
||||||
|
assertEquals(10, verifyJson.earnedMinutes)
|
||||||
|
|
||||||
|
// 6. Verify Task incorrect answer
|
||||||
|
val verifyWrongResponse =
|
||||||
|
client.post("/api/v1/tasks/verify") {
|
||||||
|
header(HttpHeaders.Authorization, "Bearer $token")
|
||||||
|
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
setBody("""{"taskId":"t1","selectedAnswer":"99"}""")
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.OK, verifyWrongResponse.status)
|
||||||
|
val verifyWrongJson = Json.decodeFromString<TaskVerifyResponse>(verifyWrongResponse.bodyAsText())
|
||||||
|
assertFalse(verifyWrongJson.isCorrect)
|
||||||
|
assertEquals(0, verifyWrongJson.earnedMinutes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package com.oliver.kidio.backend.security
|
||||||
|
|
||||||
|
import com.auth0.jwt.JWT
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
|
||||||
|
class JwtServiceTest {
|
||||||
|
@Test
|
||||||
|
fun `test generate token contains correct user id and claims`() {
|
||||||
|
// Wir nutzen ein Dummy-Secret für den Test
|
||||||
|
val service = JwtService(secret = "test-secret-für-unit-tests")
|
||||||
|
val token = service.generateToken("user-123")
|
||||||
|
|
||||||
|
assertNotNull(token)
|
||||||
|
|
||||||
|
// Token decodieren und prüfen, ob unsere Daten korrekt verpackt wurden
|
||||||
|
val decoded = JWT.decode(token)
|
||||||
|
val userIdClaim = decoded.getClaim("userId").asString()
|
||||||
|
|
||||||
|
assertEquals("user-123", userIdClaim)
|
||||||
|
assertEquals("kidio-users", decoded.audience.firstOrNull())
|
||||||
|
assertEquals("https://kidio.oliver.com/", decoded.issuer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.oliver.kidio.backend.security
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class PasswordUtilsTest {
|
||||||
|
@Test
|
||||||
|
fun `test valid passwords`() {
|
||||||
|
assertTrue(isValidPassword("StrengGeheim1!"))
|
||||||
|
assertTrue(isValidPassword("Kidio2026_Secure"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test password too short`() {
|
||||||
|
assertFalse(isValidPassword("Kurz1!"))
|
||||||
|
assertFalse(isValidPassword("Geheim!"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test missing uppercase`() {
|
||||||
|
assertFalse(isValidPassword("kleingeschrieben1!"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test missing special character`() {
|
||||||
|
assertFalse(isValidPassword("KeinSonderzeichen123"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test password with emojis is rejected`() {
|
||||||
|
assertFalse(isValidPassword("Passwort123!😊"))
|
||||||
|
assertFalse(isValidPassword("🚀Geheim123!"))
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue