Compare commits
No commits in common. "main" and "feat/fixing-security-patches" have entirely different histories.
main
...
feat/fixin
500
docs/index.html
500
docs/index.html
|
|
@ -766,21 +766,6 @@
|
||||||
"earnedMinutes" : { },
|
"earnedMinutes" : { },
|
||||||
"message" : { }
|
"message" : { }
|
||||||
}
|
}
|
||||||
};
|
|
||||||
defs["VerifyPinRequest"] = {
|
|
||||||
"title" : "VerifyPinRequest",
|
|
||||||
"required" : [ "pin" ],
|
|
||||||
"properties" : {
|
|
||||||
"pin" : { }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
defs["VerifyPinResponse"] = {
|
|
||||||
"title" : "VerifyPinResponse",
|
|
||||||
"required" : [ "message", "success" ],
|
|
||||||
"properties" : {
|
|
||||||
"success" : { },
|
|
||||||
"message" : { }
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -808,9 +793,6 @@
|
||||||
<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>
|
||||||
|
|
@ -1648,7 +1630,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></p>
|
<p>Health-Check</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right"></div>
|
<div class="pull-right"></div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
@ -1728,6 +1710,7 @@ 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) {
|
||||||
|
|
@ -1773,6 +1756,7 @@ namespace Example
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Health-Check
|
||||||
apiInstance.apiV1HealthGet();
|
apiInstance.apiV1HealthGet();
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
|
@ -1825,6 +1809,7 @@ 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>
|
||||||
|
|
@ -1891,468 +1876,6 @@ 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">
|
||||||
|
|
@ -2671,7 +2194,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></p>
|
<p>3. Bildschirmzeit synchronisieren</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right"></div>
|
<div class="pull-right"></div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
@ -2761,6 +2284,7 @@ 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) {
|
||||||
|
|
@ -2816,6 +2340,7 @@ namespace Example
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// 3. Bildschirmzeit synchronisieren
|
||||||
SyncResponse result = apiInstance.apiV1ScreentimeSyncPost(body);
|
SyncResponse result = apiInstance.apiV1ScreentimeSyncPost(body);
|
||||||
Debug.WriteLine(result);
|
Debug.WriteLine(result);
|
||||||
}
|
}
|
||||||
|
|
@ -2877,6 +2402,7 @@ 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:
|
||||||
|
|
@ -2989,7 +2515,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></p>
|
<p>1. Alle verfügbaren Aufgaben abrufen (mit optionaler Filterung nach Fach und Alter)</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right"></div>
|
<div class="pull-right"></div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
@ -3082,6 +2608,7 @@ 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
|
||||||
|
|
@ -3140,6 +2667,7 @@ 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)
|
||||||
|
|
@ -3204,6 +2732,7 @@ 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>
|
||||||
|
|
@ -3331,7 +2860,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></p>
|
<p>2. Antwort auf eine Aufgabe überprüfen</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right"></div>
|
<div class="pull-right"></div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
@ -3421,6 +2950,7 @@ 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) {
|
||||||
|
|
@ -3476,6 +3006,7 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -3537,6 +3068,7 @@ 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,14 +1,50 @@
|
||||||
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"
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
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.domain.models
|
package com.oliver.kidio.backend
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
|
@ -45,3 +45,51 @@ 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,
|
||||||
|
)
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
@ -1,242 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
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>()
|
|
||||||
|
|
||||||
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,22 +1,32 @@
|
||||||
package com.oliver.kidio.backend.domain.repository
|
package com.oliver.kidio.backend.domain.repository
|
||||||
|
|
||||||
import com.oliver.kidio.backend.domain.models.ImportedTask
|
import com.oliver.kidio.backend.ImportedTask
|
||||||
import com.oliver.kidio.backend.domain.models.Task
|
import com.oliver.kidio.backend.Task
|
||||||
import com.oliver.kidio.backend.domain.models.User
|
import com.oliver.kidio.backend.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
|
||||||
|
|
@ -39,3 +49,331 @@ 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.data.tables.TasksTable
|
import com.oliver.kidio.backend.ImportedTask
|
||||||
import com.oliver.kidio.backend.domain.models.ImportedTask
|
import com.oliver.kidio.backend.data.database.TasksTable
|
||||||
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
|
||||||
|
|
@ -21,9 +21,12 @@ object H5PIngestionCli {
|
||||||
limit: Int = 50,
|
limit: Int = 50,
|
||||||
): Int =
|
): Int =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
|
// Bereits vorhandene Task-IDs aus der Datenbank auslesen
|
||||||
val existingIds = repository.getExistingTaskIds("zum.de")
|
val existingIds = repository.getExistingTaskIds("zum.de")
|
||||||
println("Bereits in DB vorhandene ZUM-Aufgaben: ${existingIds.size}")
|
println("Bereits in DB vorhandene ZUM-Aufgaben: ${existingIds.size}")
|
||||||
|
|
||||||
|
var importedCount = 0
|
||||||
|
// existingIds an fetchLiveOerItems übergeben, um Duplikate vor dem Download zu filtern
|
||||||
val items =
|
val items =
|
||||||
client.fetchLiveOerItems(
|
client.fetchLiveOerItems(
|
||||||
subject = subject,
|
subject = subject,
|
||||||
|
|
@ -31,92 +34,65 @@ object H5PIngestionCli {
|
||||||
existingIds = existingIds,
|
existingIds = existingIds,
|
||||||
)
|
)
|
||||||
|
|
||||||
var importedCount = 0
|
|
||||||
for (item in items) {
|
for (item in items) {
|
||||||
importedCount += processItem(item, client, repository)
|
var parsedList = emptyList<ParsedH5PTask>()
|
||||||
|
|
||||||
|
// 1. Try parsing JSON content directly (fast, no ZIP decompression needed)
|
||||||
|
if (item.rawJsonContent != null) {
|
||||||
|
parsedList =
|
||||||
|
H5PParser.parseAllTasks(
|
||||||
|
contentJson = item.rawJsonContent,
|
||||||
|
library = item.library ?: "H5P.MultiChoice",
|
||||||
|
taskId = item.id,
|
||||||
|
title = item.title,
|
||||||
|
license = item.license,
|
||||||
|
sourceUrl = item.downloadUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fallback to downloading .h5p ZIP archive if remote archive is specified
|
||||||
|
if (parsedList.isEmpty() && item.downloadUrl.endsWith(".h5p", ignoreCase = true)) {
|
||||||
|
val h5pBytes = client.downloadH5P(item.downloadUrl)
|
||||||
|
if (h5pBytes != null) {
|
||||||
|
val single = H5PParser.parseH5PZip(ByteArrayInputStream(h5pBytes), item.id, item.downloadUrl)
|
||||||
|
if (single != null) {
|
||||||
|
parsedList = listOf(single)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (parsed in parsedList) {
|
||||||
|
val (minAge, maxAge) = ZumApiClient.mapGradeToAgeRange(item.grade)
|
||||||
|
val task =
|
||||||
|
ImportedTask(
|
||||||
|
id = parsed.id,
|
||||||
|
externalSource = "zum.de",
|
||||||
|
sourceUrl = parsed.sourceUrl ?: item.downloadUrl,
|
||||||
|
license = parsed.license,
|
||||||
|
subject = ZumApiClient.mapSubject(item.subject),
|
||||||
|
minAge = minAge,
|
||||||
|
maxAge = maxAge,
|
||||||
|
taskType = if (parsed.options.size == 2 && parsed.options.contains("Wahr")) "TRUE_FALSE" else "MULTIPLE_CHOICE",
|
||||||
|
question = parsed.question,
|
||||||
|
options = parsed.options,
|
||||||
|
correctAnswer = parsed.correctAnswer,
|
||||||
|
rewardMinutes = 5,
|
||||||
|
)
|
||||||
|
|
||||||
|
val success = repository.upsertTask(task)
|
||||||
|
if (success) {
|
||||||
|
importedCount++
|
||||||
|
val shortQ = task.question.take(30)
|
||||||
|
val ageRange = "${task.minAge}-${task.maxAge}"
|
||||||
|
println(" -> [Importiert] ${task.id} (${task.subject}, $ageRange): \"$shortQ...\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println("Ingestion completed: $importedCount tasks successfully imported/updated.")
|
println("Ingestion completed: $importedCount tasks successfully imported/updated.")
|
||||||
importedCount
|
importedCount
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun processItem(
|
|
||||||
item: ZumOerItem,
|
|
||||||
client: ZumApiClient,
|
|
||||||
repository: KidioRepository,
|
|
||||||
): Int {
|
|
||||||
var importedCount = 0
|
|
||||||
val parsedList = extractTasks(item, client)
|
|
||||||
|
|
||||||
for (parsed in parsedList) {
|
|
||||||
val task = mapToImportedTask(item, parsed)
|
|
||||||
if (repository.upsertTask(task)) {
|
|
||||||
importedCount++
|
|
||||||
logSuccess(task)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return importedCount
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun extractTasks(
|
|
||||||
item: ZumOerItem,
|
|
||||||
client: ZumApiClient,
|
|
||||||
): List<ParsedH5PTask> {
|
|
||||||
if (item.rawJsonContent != null) {
|
|
||||||
val tasks =
|
|
||||||
H5PParser.parseAllTasks(
|
|
||||||
contentJson = item.rawJsonContent,
|
|
||||||
library = item.library ?: "H5P.MultiChoice",
|
|
||||||
taskId = item.id,
|
|
||||||
title = item.title,
|
|
||||||
license = item.license,
|
|
||||||
sourceUrl = item.downloadUrl,
|
|
||||||
)
|
|
||||||
if (tasks.isNotEmpty()) return tasks
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.downloadUrl.endsWith(".h5p", ignoreCase = true)) {
|
|
||||||
val h5pBytes = client.downloadH5P(item.downloadUrl) ?: return emptyList()
|
|
||||||
val single = H5PParser.parseH5PZip(ByteArrayInputStream(h5pBytes), item.id, item.downloadUrl)
|
|
||||||
|
|
||||||
if (single != null) {
|
|
||||||
return listOf(single)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mapToImportedTask(
|
|
||||||
item: ZumOerItem,
|
|
||||||
parsed: ParsedH5PTask,
|
|
||||||
): ImportedTask {
|
|
||||||
val (minAge, maxAge) = ZumApiClient.mapGradeToAgeRange(item.grade)
|
|
||||||
|
|
||||||
val isTrueFalse = parsed.options.size == 2 && parsed.options.contains("Wahr")
|
|
||||||
|
|
||||||
return ImportedTask(
|
|
||||||
id = parsed.id,
|
|
||||||
externalSource = "zum.de",
|
|
||||||
sourceUrl = parsed.sourceUrl ?: item.downloadUrl,
|
|
||||||
license = parsed.license,
|
|
||||||
subject = ZumApiClient.mapSubject(item.subject),
|
|
||||||
minAge = minAge,
|
|
||||||
maxAge = maxAge,
|
|
||||||
taskType = if (isTrueFalse) "TRUE_FALSE" else "MULTIPLE_CHOICE",
|
|
||||||
question = parsed.question,
|
|
||||||
options = parsed.options,
|
|
||||||
correctAnswer = parsed.correctAnswer,
|
|
||||||
rewardMinutes = 5,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun logSuccess(task: ImportedTask) {
|
|
||||||
val shortQ = task.question.take(30)
|
|
||||||
val ageRange = "${task.minAge}-${task.maxAge}"
|
|
||||||
println(" -> [Importiert] ${task.id} (${task.subject}, $ageRange): \"$shortQ...\"")
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
println("=== Kidio H5P/ZUM Ingestion Tool ===")
|
println("=== Kidio H5P/ZUM Ingestion Tool ===")
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,17 @@ import kotlinx.serialization.json.*
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
|
data class ParsedH5PTask(
|
||||||
|
val id: String,
|
||||||
|
val title: String,
|
||||||
|
val library: String,
|
||||||
|
val question: String,
|
||||||
|
val options: List<String>,
|
||||||
|
val correctAnswer: String,
|
||||||
|
val license: String = "CC BY-SA 4.0",
|
||||||
|
val sourceUrl: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
object H5PParser {
|
object H5PParser {
|
||||||
private val json =
|
private val json =
|
||||||
Json {
|
Json {
|
||||||
|
|
@ -68,9 +79,9 @@ object H5PParser {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
var library = DEFAULT_LIBRARY
|
var library = "H5P.MultiChoice"
|
||||||
var title = DEFAULT_TITLE
|
var title = "H5P Task"
|
||||||
var license = DEFAULT_LICENSE
|
var license = "CC BY-SA 4.0"
|
||||||
|
|
||||||
if (h5pJsonString != null) {
|
if (h5pJsonString != null) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -78,8 +89,7 @@ object H5PParser {
|
||||||
library = h5pTree["mainLibrary"]?.jsonPrimitive?.contentOrNull ?: library
|
library = h5pTree["mainLibrary"]?.jsonPrimitive?.contentOrNull ?: library
|
||||||
title = h5pTree["title"]?.jsonPrimitive?.contentOrNull ?: title
|
title = h5pTree["title"]?.jsonPrimitive?.contentOrNull ?: title
|
||||||
license = h5pTree["license"]?.jsonPrimitive?.contentOrNull ?: license
|
license = h5pTree["license"]?.jsonPrimitive?.contentOrNull ?: license
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
println("WARNUNG: h5p.json konnte nicht gelesen werden. Nutze Fallback-Werte. Fehler: ${e.message}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,10 +108,10 @@ object H5PParser {
|
||||||
*/
|
*/
|
||||||
fun parseContentJson(
|
fun parseContentJson(
|
||||||
contentJson: String,
|
contentJson: String,
|
||||||
library: String = DEFAULT_LIBRARY,
|
library: String = "H5P.MultiChoice",
|
||||||
taskId: String = "task-1",
|
taskId: String = "task-1",
|
||||||
title: String = DEFAULT_TITLE,
|
title: String = "H5P Task",
|
||||||
license: String = DEFAULT_LICENSE,
|
license: String = "CC BY-SA 4.0",
|
||||||
sourceUrl: String? = null,
|
sourceUrl: String? = null,
|
||||||
): ParsedH5PTask? {
|
): ParsedH5PTask? {
|
||||||
val element =
|
val element =
|
||||||
|
|
@ -123,10 +133,10 @@ object H5PParser {
|
||||||
*/
|
*/
|
||||||
fun parseAllTasks(
|
fun parseAllTasks(
|
||||||
contentJson: String,
|
contentJson: String,
|
||||||
library: String = DEFAULT_LIBRARY,
|
library: String = "H5P.MultiChoice",
|
||||||
taskId: String = "task-1",
|
taskId: String = "task-1",
|
||||||
title: String = DEFAULT_TITLE,
|
title: String = "H5P Task",
|
||||||
license: String = DEFAULT_LICENSE,
|
license: String = "CC BY-SA 4.0",
|
||||||
sourceUrl: String? = null,
|
sourceUrl: String? = null,
|
||||||
): List<ParsedH5PTask> {
|
): List<ParsedH5PTask> {
|
||||||
val element =
|
val element =
|
||||||
|
|
@ -138,46 +148,40 @@ object H5PParser {
|
||||||
|
|
||||||
if (library.contains("QuestionSet", ignoreCase = true) && element.containsKey("questions")) {
|
if (library.contains("QuestionSet", ignoreCase = true) && element.containsKey("questions")) {
|
||||||
val questionsArray = element["questions"]?.jsonArray ?: return emptyList()
|
val questionsArray = element["questions"]?.jsonArray ?: return emptyList()
|
||||||
|
val results = mutableListOf<ParsedH5PTask>()
|
||||||
|
|
||||||
return questionsArray.mapIndexedNotNull { idx, qElement ->
|
for ((idx, qElement) in questionsArray.withIndex()) {
|
||||||
parseQuestionSetItem(qElement, idx, taskId, title, license, sourceUrl)
|
val qObj = qElement.jsonObject
|
||||||
|
val subLib = qObj["library"]?.jsonPrimitive?.contentOrNull ?: "H5P.MultiChoice"
|
||||||
|
val subParams = qObj["params"]?.jsonObject ?: continue
|
||||||
|
val subId = "$taskId-q${idx + 1}"
|
||||||
|
|
||||||
|
val isTrueFalse =
|
||||||
|
subLib.contains("TrueFalse", ignoreCase = true) ||
|
||||||
|
(subParams.containsKey("correct") && !subParams.containsKey("answers"))
|
||||||
|
val isMultiChoice =
|
||||||
|
subLib.contains("MultiChoice", ignoreCase = true) || subParams.containsKey("answers")
|
||||||
|
|
||||||
|
val parsedSub =
|
||||||
|
if (isTrueFalse) {
|
||||||
|
parseTrueFalse(subParams, subId, "$title #${idx + 1}", subLib, license, sourceUrl)
|
||||||
|
} else if (isMultiChoice) {
|
||||||
|
parseMultiChoice(subParams, subId, "$title #${idx + 1}", subLib, license, sourceUrl)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedSub != null) {
|
||||||
|
results.add(parsedSub)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
val single = parseContentJson(contentJson, library, taskId, title, license, sourceUrl)
|
val single = parseContentJson(contentJson, library, taskId, title, license, sourceUrl)
|
||||||
return if (single != null) listOf(single) else emptyList()
|
return if (single != null) listOf(single) else emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseQuestionSetItem(
|
|
||||||
qElement: JsonElement,
|
|
||||||
idx: Int,
|
|
||||||
taskId: String,
|
|
||||||
title: String,
|
|
||||||
license: String,
|
|
||||||
sourceUrl: String?,
|
|
||||||
): ParsedH5PTask? {
|
|
||||||
val qObj = qElement.jsonObject
|
|
||||||
val subLib = qObj["library"]?.jsonPrimitive?.contentOrNull ?: DEFAULT_LIBRARY
|
|
||||||
val subParams = qObj["params"]?.jsonObject ?: return null
|
|
||||||
|
|
||||||
val subId = "$taskId-q${idx + 1}"
|
|
||||||
val subTitle = "$title #${idx + 1}"
|
|
||||||
|
|
||||||
val isTrueFalse =
|
|
||||||
subLib.contains("TrueFalse", ignoreCase = true) ||
|
|
||||||
(subParams.containsKey("correct") && !subParams.containsKey("answers"))
|
|
||||||
|
|
||||||
val isMultiChoice =
|
|
||||||
subLib.contains("MultiChoice", ignoreCase = true) ||
|
|
||||||
subParams.containsKey("answers")
|
|
||||||
|
|
||||||
return when {
|
|
||||||
isTrueFalse -> parseTrueFalse(subParams, subId, subTitle, subLib, license, sourceUrl)
|
|
||||||
isMultiChoice -> parseMultiChoice(subParams, subId, subTitle, subLib, license, sourceUrl)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseMultiChoice(
|
private fun parseMultiChoice(
|
||||||
element: JsonObject,
|
element: JsonObject,
|
||||||
taskId: String,
|
taskId: String,
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
package com.oliver.kidio.backend.ingestion
|
|
||||||
|
|
||||||
const val DEFAULT_LIBRARY: String = "H5P.MultiChoice"
|
|
||||||
const val DEFAULT_TITLE: String = "H5P Task"
|
|
||||||
|
|
||||||
data class ParsedH5PTask(
|
|
||||||
val id: String,
|
|
||||||
val title: String,
|
|
||||||
val library: String,
|
|
||||||
val question: String,
|
|
||||||
val options: List<String>,
|
|
||||||
val correctAnswer: String,
|
|
||||||
val license: String = DEFAULT_LICENSE,
|
|
||||||
val sourceUrl: String? = null,
|
|
||||||
)
|
|
||||||
|
|
@ -8,6 +8,17 @@ import java.net.http.HttpRequest
|
||||||
import java.net.http.HttpResponse
|
import java.net.http.HttpResponse
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
|
||||||
|
data class ZumOerItem(
|
||||||
|
val id: String,
|
||||||
|
val title: String,
|
||||||
|
val subject: String,
|
||||||
|
val grade: Int? = null,
|
||||||
|
val downloadUrl: String,
|
||||||
|
val license: String = "CC BY-SA 4.0",
|
||||||
|
val library: String? = null,
|
||||||
|
val rawJsonContent: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
class ZumApiClient(
|
class ZumApiClient(
|
||||||
private val httpClient: HttpClient =
|
private val httpClient: HttpClient =
|
||||||
HttpClient.newBuilder()
|
HttpClient.newBuilder()
|
||||||
|
|
@ -102,7 +113,52 @@ class ZumApiClient(
|
||||||
|
|
||||||
for (page in 0 until maxPages) {
|
for (page in 0 until maxPages) {
|
||||||
if (results.size >= limit) break
|
if (results.size >= limit) break
|
||||||
processPage(page, subject, limit, existingIds, seenAppIds, results)
|
|
||||||
|
val searchUrl =
|
||||||
|
if (subject.isNullOrBlank()) {
|
||||||
|
"https://apps.zum.de/apps?page=$page"
|
||||||
|
} else {
|
||||||
|
val encoded = URLEncoder.encode(subject, "UTF-8")
|
||||||
|
"https://apps.zum.de/apps?keys=$encoded&page=$page"
|
||||||
|
}
|
||||||
|
|
||||||
|
val html = fetchString(searchUrl)
|
||||||
|
if (html == null) {
|
||||||
|
println("[ZUM-Apps Live] Warnung: Seite $page konnte nicht geladen werden ($searchUrl)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtert bereits verarbeitete oder in DB vorhandene Apps heraus
|
||||||
|
val appIds =
|
||||||
|
Regex("""href=["']/apps/(\d+)["']""")
|
||||||
|
.findAll(html)
|
||||||
|
.map { it.groupValues[1] }
|
||||||
|
.distinct()
|
||||||
|
.filter { appId ->
|
||||||
|
val fullId = "zum-$appId"
|
||||||
|
!seenAppIds.contains(appId) && !existingIds.contains(fullId) && !existingIds.contains(appId)
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
if (appIds.isEmpty()) {
|
||||||
|
println("[ZUM-Apps Live] Seite $page enthält keine neuen Aufgaben. Blättere weiter...")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for (appId in appIds) {
|
||||||
|
if (results.size >= limit) break
|
||||||
|
seenAppIds.add(appId)
|
||||||
|
|
||||||
|
val item = fetchAppDetails(appId, subject)
|
||||||
|
if (item != null) {
|
||||||
|
results.add(item)
|
||||||
|
val lib = item.library ?: "H5P"
|
||||||
|
val gr = item.grade ?: "k.A."
|
||||||
|
println(
|
||||||
|
"[ZUM-Apps Live] Neue App #$appId: \"${item.title}\" ($lib) [${item.subject}, Kl. $gr]",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (results.isEmpty()) {
|
if (results.isEmpty()) {
|
||||||
|
|
@ -114,78 +170,6 @@ class ZumApiClient(
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processPage(
|
|
||||||
page: Int,
|
|
||||||
subject: String?,
|
|
||||||
limit: Int,
|
|
||||||
existingIds: Set<String>,
|
|
||||||
seenAppIds: MutableSet<String>,
|
|
||||||
results: MutableList<ZumOerItem>,
|
|
||||||
) {
|
|
||||||
val searchUrl = buildSearchUrl(subject, page)
|
|
||||||
val html = fetchString(searchUrl)
|
|
||||||
|
|
||||||
if (html == null) {
|
|
||||||
println("[ZUM-Apps Live] Warnung: Seite $page konnte nicht geladen werden ($searchUrl)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val appIds = extractNewAppIds(html, existingIds, seenAppIds)
|
|
||||||
|
|
||||||
if (appIds.isEmpty()) {
|
|
||||||
println("[ZUM-Apps Live] Seite $page enthält keine neuen Aufgaben. Blättere weiter...")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (appId in appIds) {
|
|
||||||
if (results.size >= limit) break
|
|
||||||
seenAppIds.add(appId)
|
|
||||||
|
|
||||||
val item = fetchAppDetails(appId, subject)
|
|
||||||
if (item != null) {
|
|
||||||
results.add(item)
|
|
||||||
logNewItem(appId, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildSearchUrl(
|
|
||||||
subject: String?,
|
|
||||||
page: Int,
|
|
||||||
): String {
|
|
||||||
return if (subject.isNullOrBlank()) {
|
|
||||||
"https://apps.zum.de/apps?page=$page"
|
|
||||||
} else {
|
|
||||||
val encoded = URLEncoder.encode(subject, "UTF-8")
|
|
||||||
"https://apps.zum.de/apps?keys=$encoded&page=$page"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun extractNewAppIds(
|
|
||||||
html: String,
|
|
||||||
existingIds: Set<String>,
|
|
||||||
seenAppIds: Set<String>,
|
|
||||||
): List<String> {
|
|
||||||
return Regex("""href=["']/apps/(\d+)["']""")
|
|
||||||
.findAll(html)
|
|
||||||
.map { it.groupValues[1] }
|
|
||||||
.distinct()
|
|
||||||
.filter { appId ->
|
|
||||||
val fullId = "zum-$appId"
|
|
||||||
!seenAppIds.contains(appId) && !existingIds.contains(fullId) && !existingIds.contains(appId)
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun logNewItem(
|
|
||||||
appId: String,
|
|
||||||
item: ZumOerItem,
|
|
||||||
) {
|
|
||||||
val lib = item.library ?: "H5P"
|
|
||||||
val gr = item.grade ?: "k.A."
|
|
||||||
println("[ZUM-Apps Live] Neue App #$appId: \"${item.title}\" ($lib) [${item.subject}, Kl. $gr]")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchAppDetails(
|
private fun fetchAppDetails(
|
||||||
appId: String,
|
appId: String,
|
||||||
fallbackSubject: String?,
|
fallbackSubject: String?,
|
||||||
|
|
@ -232,7 +216,7 @@ class ZumApiClient(
|
||||||
val rawLicense = metadata?.get("license")?.jsonPrimitive?.contentOrNull
|
val rawLicense = metadata?.get("license")?.jsonPrimitive?.contentOrNull
|
||||||
val license =
|
val license =
|
||||||
when {
|
when {
|
||||||
rawLicense == "U" || rawLicense == null || rawLicense.isBlank() -> DEFAULT_LICENSE
|
rawLicense == "U" || rawLicense == null || rawLicense.isBlank() -> "CC BY-SA 4.0"
|
||||||
rawLicense.contains("CC", ignoreCase = true) -> rawLicense
|
rawLicense.contains("CC", ignoreCase = true) -> rawLicense
|
||||||
else -> "CC BY-SA 4.0 ($rawLicense)"
|
else -> "CC BY-SA 4.0 ($rawLicense)"
|
||||||
}
|
}
|
||||||
|
|
@ -354,26 +338,168 @@ class ZumApiClient(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns curated ZUM/OER sample metadata items from resources, filterable by subject.
|
* Returns curated ZUM/OER sample metadata items, filterable by subject.
|
||||||
*/
|
*/
|
||||||
fun getCuratedOerItems(
|
fun getCuratedOerItems(
|
||||||
subject: String? = null,
|
subject: String? = null,
|
||||||
limit: Int = 20,
|
limit: Int = 20,
|
||||||
): List<ZumOerItem> {
|
): List<ZumOerItem> {
|
||||||
val allItems: List<ZumOerItem> =
|
val allItems =
|
||||||
try {
|
listOf(
|
||||||
val inputStream = this::class.java.getResourceAsStream("/fallback_oer_items.json")
|
ZumOerItem(
|
||||||
if (inputStream != null) {
|
id = "zum-math-001",
|
||||||
val jsonText = inputStream.bufferedReader().use { it.readText() }
|
title = "Grundrechenarten: Multiplikation 3. Klasse",
|
||||||
json.decodeFromString(jsonText)
|
subject = "Mathematik",
|
||||||
} else {
|
grade = 3,
|
||||||
println("WARNUNG: fallback_oer_items.json wurde nicht gefunden!")
|
downloadUrl = "https://apps.zum.de/api/h5p/math-001.h5p",
|
||||||
emptyList()
|
license = "CC BY-SA 4.0",
|
||||||
}
|
rawJsonContent =
|
||||||
} catch (e: Exception) {
|
"""
|
||||||
println("WARNUNG: Fehler beim Laden der Fallback-Daten: ${e.message}")
|
{
|
||||||
emptyList()
|
"question": "<p>Was ist das Ergebnis von <strong>6 × 8</strong>?</p>",
|
||||||
}
|
"answers": [
|
||||||
|
{"text": "42", "correct": false},
|
||||||
|
{"text": "48", "correct": true},
|
||||||
|
{"text": "54", "correct": false},
|
||||||
|
{"text": "56", "correct": false}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
),
|
||||||
|
ZumOerItem(
|
||||||
|
id = "zum-math-002",
|
||||||
|
title = "Zahlenraum bis 100: Addition",
|
||||||
|
subject = "Mathematik",
|
||||||
|
grade = 2,
|
||||||
|
downloadUrl = "https://apps.zum.de/api/h5p/math-002.h5p",
|
||||||
|
license = "CC BY-SA 4.0",
|
||||||
|
rawJsonContent =
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"question": "<p>Berechne: 34 + 28</p>",
|
||||||
|
"answers": [
|
||||||
|
{"text": "52", "correct": false},
|
||||||
|
{"text": "62", "correct": true},
|
||||||
|
{"text": "64", "correct": false}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
),
|
||||||
|
ZumOerItem(
|
||||||
|
id = "zum-math-003",
|
||||||
|
title = "Geometrie: Formen erkennen",
|
||||||
|
subject = "Mathematik",
|
||||||
|
grade = 4,
|
||||||
|
downloadUrl = "https://apps.zum.de/api/h5p/math-003.h5p",
|
||||||
|
license = "CC BY-SA 4.0",
|
||||||
|
rawJsonContent =
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"question": "<p>Ein Quadrat hat vier gleich lange Seiten und vier rechte Winkel.</p>",
|
||||||
|
"correct": true
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
),
|
||||||
|
ZumOerItem(
|
||||||
|
id = "zum-de-001",
|
||||||
|
title = "Wortarten: Nomen erkennen",
|
||||||
|
subject = "Deutsch",
|
||||||
|
grade = 2,
|
||||||
|
downloadUrl = "https://apps.zum.de/api/h5p/de-001.h5p",
|
||||||
|
license = "CC BY-SA 4.0",
|
||||||
|
rawJsonContent =
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"question": "<p>Welches der folgenden Wörter ist ein <strong>Nomen (Namenwort)</strong>?</p>",
|
||||||
|
"answers": [
|
||||||
|
{"text": "schnell", "correct": false},
|
||||||
|
{"text": "laufen", "correct": false},
|
||||||
|
{"text": "Schule", "correct": true},
|
||||||
|
{"text": "schön", "correct": false}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
),
|
||||||
|
ZumOerItem(
|
||||||
|
id = "zum-de-002",
|
||||||
|
title = "Rechtschreibung: Doppel-s oder ss",
|
||||||
|
subject = "Deutsch",
|
||||||
|
grade = 4,
|
||||||
|
downloadUrl = "https://apps.zum.de/api/h5p/de-002.h5p",
|
||||||
|
license = "CC BY-SA 4.0",
|
||||||
|
rawJsonContent =
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"question": "<p>Wie wird das Wort richtig geschrieben? „Der Flu... ist tief.“</p>",
|
||||||
|
"answers": [
|
||||||
|
{"text": "Flus", "correct": false},
|
||||||
|
{"text": "Fluss", "correct": true},
|
||||||
|
{"text": "Fluß", "correct": false}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
),
|
||||||
|
ZumOerItem(
|
||||||
|
id = "zum-sach-001",
|
||||||
|
title = "Natur & Tiere: Winterschlaf",
|
||||||
|
subject = "Sachunterricht",
|
||||||
|
grade = 3,
|
||||||
|
downloadUrl = "https://apps.zum.de/api/h5p/sach-001.h5p",
|
||||||
|
license = "CC BY-SA 4.0",
|
||||||
|
rawJsonContent =
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"question": "<p>Welches dieser Tiere hält einen echten <strong>Winterschlaf</strong>?</p>",
|
||||||
|
"answers": [
|
||||||
|
{"text": "Igel", "correct": true},
|
||||||
|
{"text": "Eichhörnchen", "correct": false},
|
||||||
|
{"text": "Amsel", "correct": false},
|
||||||
|
{"text": "Fuchs", "correct": false}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
),
|
||||||
|
ZumOerItem(
|
||||||
|
id = "zum-sach-002",
|
||||||
|
title = "Planetensystem: Sonnennähe",
|
||||||
|
subject = "Sachunterricht",
|
||||||
|
grade = 4,
|
||||||
|
downloadUrl = "https://apps.zum.de/api/h5p/sach-002.h5p",
|
||||||
|
license = "CC BY-SA 4.0",
|
||||||
|
rawJsonContent =
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"question": "<p>Welcher Planet ist der Sonne am nächsten?</p>",
|
||||||
|
"answers": [
|
||||||
|
{"text": "Venus", "correct": false},
|
||||||
|
{"text": "Mars", "correct": false},
|
||||||
|
{"text": "Merkur", "correct": true},
|
||||||
|
{"text": "Jupiter", "correct": false}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
),
|
||||||
|
ZumOerItem(
|
||||||
|
id = "zum-eng-001",
|
||||||
|
title = "English Vocabulary: Colors & Animals",
|
||||||
|
subject = "Englisch",
|
||||||
|
grade = 4,
|
||||||
|
downloadUrl = "https://apps.zum.de/api/h5p/eng-001.h5p",
|
||||||
|
license = "CC BY-SA 4.0",
|
||||||
|
rawJsonContent =
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"question": "<p>What is the English word for „Hund“?</p>",
|
||||||
|
"answers": [
|
||||||
|
{"text": "Cat", "correct": false},
|
||||||
|
{"text": "Dog", "correct": true},
|
||||||
|
{"text": "Bird", "correct": false},
|
||||||
|
{"text": "Rabbit", "correct": false}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
val normalizedSubject = subject?.let { mapSubject(it) }
|
val normalizedSubject = subject?.let { mapSubject(it) }
|
||||||
val filtered =
|
val filtered =
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package com.oliver.kidio.backend.ingestion
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
const val DEFAULT_LICENSE: String = "CC BY-SA 4.0"
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ZumOerItem(
|
|
||||||
val id: String,
|
|
||||||
val title: String,
|
|
||||||
val subject: String,
|
|
||||||
val grade: Int? = null,
|
|
||||||
val downloadUrl: String,
|
|
||||||
val license: String = DEFAULT_LICENSE,
|
|
||||||
val library: String? = null,
|
|
||||||
val rawJsonContent: String? = null,
|
|
||||||
)
|
|
||||||
|
|
@ -1,12 +1,38 @@
|
||||||
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,
|
||||||
|
|
@ -15,13 +41,195 @@ 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!"))
|
||||||
}
|
}
|
||||||
|
|
||||||
authRoutes(repository, jwtService)
|
post("/api/v1/auth/login") {
|
||||||
taskRoutes(repository)
|
val loginRequest = call.receive<LoginRequest>()
|
||||||
screentimeRoutes(repository)
|
if (loginRequest.email.isBlank() || loginRequest.passwordPlain.isBlank()) {
|
||||||
parentRoutes(repository)
|
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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
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,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
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."),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
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,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
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!",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
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,74 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": "zum-math-001",
|
|
||||||
"title": "Grundrechenarten: Multiplikation 3. Klasse",
|
|
||||||
"subject": "Mathematik",
|
|
||||||
"grade": 3,
|
|
||||||
"downloadUrl": "https://apps.zum.de/api/h5p/math-001.h5p",
|
|
||||||
"license": "CC BY-SA 4.0",
|
|
||||||
"rawJsonContent": "{ \"question\": \"<p>Was ist das Ergebnis von <strong>6 × 8</strong>?</p>\", \"answers\": [ {\"text\": \"42\", \"correct\": false}, {\"text\": \"48\", \"correct\": true}, {\"text\": \"54\", \"correct\": false}, {\"text\": \"56\", \"correct\": false} ] }"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "zum-math-002",
|
|
||||||
"title": "Zahlenraum bis 100: Addition",
|
|
||||||
"subject": "Mathematik",
|
|
||||||
"grade": 2,
|
|
||||||
"downloadUrl": "https://apps.zum.de/api/h5p/math-002.h5p",
|
|
||||||
"license": "CC BY-SA 4.0",
|
|
||||||
"rawJsonContent": "{ \"question\": \"<p>Berechne: 34 + 28</p>\", \"answers\": [ {\"text\": \"52\", \"correct\": false}, {\"text\": \"62\", \"correct\": true}, {\"text\": \"64\", \"correct\": false} ] }"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "zum-math-003",
|
|
||||||
"title": "Geometrie: Formen erkennen",
|
|
||||||
"subject": "Mathematik",
|
|
||||||
"grade": 4,
|
|
||||||
"downloadUrl": "https://apps.zum.de/api/h5p/math-003.h5p",
|
|
||||||
"license": "CC BY-SA 4.0",
|
|
||||||
"rawJsonContent": "{ \"question\": \"<p>Ein Quadrat hat vier gleich lange Seiten und vier rechte Winkel.</p>\", \"correct\": true }"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "zum-de-001",
|
|
||||||
"title": "Wortarten: Nomen erkennen",
|
|
||||||
"subject": "Deutsch",
|
|
||||||
"grade": 2,
|
|
||||||
"downloadUrl": "https://apps.zum.de/api/h5p/de-001.h5p",
|
|
||||||
"license": "CC BY-SA 4.0",
|
|
||||||
"rawJsonContent": "{ \"question\": \"<p>Welches der folgenden Wörter ist ein <strong>Nomen (Namenwort)</strong>?</p>\", \"answers\": [ {\"text\": \"schnell\", \"correct\": false}, {\"text\": \"laufen\", \"correct\": false}, {\"text\": \"Schule\", \"correct\": true}, {\"text\": \"schön\", \"correct\": false} ] }"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "zum-de-002",
|
|
||||||
"title": "Rechtschreibung: Doppel-s oder ss",
|
|
||||||
"subject": "Deutsch",
|
|
||||||
"grade": 4,
|
|
||||||
"downloadUrl": "https://apps.zum.de/api/h5p/de-002.h5p",
|
|
||||||
"license": "CC BY-SA 4.0",
|
|
||||||
"rawJsonContent": "{ \"question\": \"<p>Wie wird das Wort richtig geschrieben? „Der Flu... ist tief.“</p>\", \"answers\": [ {\"text\": \"Flus\", \"correct\": false}, {\"text\": \"Fluss\", \"correct\": true}, {\"text\": \"Fluß\", \"correct\": false} ] }"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "zum-sach-001",
|
|
||||||
"title": "Natur & Tiere: Winterschlaf",
|
|
||||||
"subject": "Sachunterricht",
|
|
||||||
"grade": 3,
|
|
||||||
"downloadUrl": "https://apps.zum.de/api/h5p/sach-001.h5p",
|
|
||||||
"license": "CC BY-SA 4.0",
|
|
||||||
"rawJsonContent": "{ \"question\": \"<p>Welches dieser Tiere hält einen echten <strong>Winterschlaf</strong>?</p>\", \"answers\": [ {\"text\": \"Igel\", \"correct\": true}, {\"text\": \"Eichhörnchen\", \"correct\": false}, {\"text\": \"Amsel\", \"correct\": false}, {\"text\": \"Fuchs\", \"correct\": false} ] }"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "zum-sach-002",
|
|
||||||
"title": "Planetensystem: Sonnennähe",
|
|
||||||
"subject": "Sachunterricht",
|
|
||||||
"grade": 4,
|
|
||||||
"downloadUrl": "https://apps.zum.de/api/h5p/sach-002.h5p",
|
|
||||||
"license": "CC BY-SA 4.0",
|
|
||||||
"rawJsonContent": "{ \"question\": \"<p>Welcher Planet ist der Sonne am nächsten?</p>\", \"answers\": [ {\"text\": \"Venus\", \"correct\": false}, {\"text\": \"Mars\", \"correct\": false}, {\"text\": \"Merkur\", \"correct\": true}, {\"text\": \"Jupiter\", \"correct\": false} ] }"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "zum-eng-001",
|
|
||||||
"title": "English Vocabulary: Colors & Animals",
|
|
||||||
"subject": "Englisch",
|
|
||||||
"grade": 4,
|
|
||||||
"downloadUrl": "https://apps.zum.de/api/h5p/eng-001.h5p",
|
|
||||||
"license": "CC BY-SA 4.0",
|
|
||||||
"rawJsonContent": "{ \"question\": \"<p>What is the English word for „Hund“?</p>\", \"answers\": [ {\"text\": \"Cat\", \"correct\": false}, {\"text\": \"Dog\", \"correct\": true}, {\"text\": \"Bird\", \"correct\": false}, {\"text\": \"Rabbit\", \"correct\": false} ] }"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.oliver.kidio.backend.com.oliver.kidio.backend.ingestion
|
package com.oliver.kidio.backend
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
package com.oliver.kidio.backend.routing
|
package com.oliver.kidio.backend
|
||||||
|
|
||||||
import com.oliver.kidio.backend.domain.models.AuthResponse
|
|
||||||
import com.oliver.kidio.backend.domain.models.ImportedTask
|
|
||||||
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
|
||||||
|
|
@ -20,7 +15,6 @@ import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
import io.ktor.server.application.install
|
import io.ktor.server.application.install
|
||||||
import io.ktor.server.testing.testApplication
|
import io.ktor.server.testing.testApplication
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
@ -36,41 +30,6 @@ class ServerTest {
|
||||||
val jwtService = JwtService()
|
val jwtService = JwtService()
|
||||||
val repository = InMemoryKidioRepository()
|
val repository = InMemoryKidioRepository()
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
repository.upsertTask(
|
|
||||||
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",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
repository.upsertTask(
|
|
||||||
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",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
application {
|
application {
|
||||||
install(ServerContentNegotiation) {
|
install(ServerContentNegotiation) {
|
||||||
json()
|
json()
|
||||||
|
|
@ -230,41 +189,6 @@ class ServerTest {
|
||||||
val jwtService = JwtService()
|
val jwtService = JwtService()
|
||||||
val repository = InMemoryKidioRepository()
|
val repository = InMemoryKidioRepository()
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
repository.upsertTask(
|
|
||||||
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",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
repository.upsertTask(
|
|
||||||
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",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
application {
|
application {
|
||||||
install(ServerContentNegotiation) {
|
install(ServerContentNegotiation) {
|
||||||
json()
|
json()
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
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!",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
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!"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
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,157 +0,0 @@
|
||||||
package com.oliver.kidio.backend.routing
|
|
||||||
|
|
||||||
import com.oliver.kidio.backend.domain.models.ImportedTask
|
|
||||||
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.coroutines.runBlocking
|
|
||||||
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()
|
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
repository.upsertTask(
|
|
||||||
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",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
repository.upsertTask(
|
|
||||||
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",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
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