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" : { },
|
||||
"message" : { }
|
||||
}
|
||||
};
|
||||
defs["VerifyPinRequest"] = {
|
||||
"title" : "VerifyPinRequest",
|
||||
"required" : [ "pin" ],
|
||||
"properties" : {
|
||||
"pin" : { }
|
||||
}
|
||||
};
|
||||
defs["VerifyPinResponse"] = {
|
||||
"title" : "VerifyPinResponse",
|
||||
"required" : [ "message", "success" ],
|
||||
"properties" : {
|
||||
"success" : { },
|
||||
"message" : { }
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -808,9 +793,6 @@
|
|||
<li data-group="Default" data-name="apiV1HealthGet" class="">
|
||||
<a href="#api-Default-apiV1HealthGet">apiV1HealthGet</a>
|
||||
</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="">
|
||||
<a href="#api-Default-apiV1ScreentimeGet">apiV1ScreentimeGet</a>
|
||||
</li>
|
||||
|
|
@ -1648,7 +1630,7 @@ except ApiException as e:
|
|||
<article id="api-Default-apiV1HealthGet-0" data-group="User" data-name="apiV1HealthGet" data-version="0">
|
||||
<div class="pull-left">
|
||||
<h1>apiV1HealthGet</h1>
|
||||
<p></p>
|
||||
<p>Health-Check</p>
|
||||
</div>
|
||||
<div class="pull-right"></div>
|
||||
<div class="clearfix"></div>
|
||||
|
|
@ -1728,6 +1710,7 @@ public class DefaultApiExample {
|
|||
<pre class="prettyprint"><code class="language-cpp">
|
||||
DefaultApi *apiInstance = [[DefaultApi alloc] init];
|
||||
|
||||
// Health-Check
|
||||
[apiInstance apiV1HealthGetWithCompletionHandler:
|
||||
^(NSError* error) {
|
||||
if (error) {
|
||||
|
|
@ -1773,6 +1756,7 @@ namespace Example
|
|||
|
||||
try
|
||||
{
|
||||
// Health-Check
|
||||
apiInstance.apiV1HealthGet();
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
@ -1825,6 +1809,7 @@ from pprint import pprint
|
|||
api_instance = swagger_client.DefaultApi()
|
||||
|
||||
try:
|
||||
# Health-Check
|
||||
api_instance.api_v1_health_get()
|
||||
except ApiException as e:
|
||||
print("Exception when calling DefaultApi->apiV1HealthGet: %s\n" % e)</code></pre>
|
||||
|
|
@ -1891,468 +1876,6 @@ except ApiException as e:
|
|||
</article>
|
||||
</div>
|
||||
<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">
|
||||
<article id="api-Default-apiV1ScreentimeGet-0" data-group="User" data-name="apiV1ScreentimeGet" data-version="0">
|
||||
<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">
|
||||
<div class="pull-left">
|
||||
<h1>apiV1ScreentimeSyncPost</h1>
|
||||
<p></p>
|
||||
<p>3. Bildschirmzeit synchronisieren</p>
|
||||
</div>
|
||||
<div class="pull-right"></div>
|
||||
<div class="clearfix"></div>
|
||||
|
|
@ -2761,6 +2284,7 @@ SyncRequest *body = ; // (optional)
|
|||
|
||||
DefaultApi *apiInstance = [[DefaultApi alloc] init];
|
||||
|
||||
// 3. Bildschirmzeit synchronisieren
|
||||
[apiInstance apiV1ScreentimeSyncPostWith:body
|
||||
completionHandler: ^(SyncResponse output, NSError* error) {
|
||||
if (output) {
|
||||
|
|
@ -2816,6 +2340,7 @@ namespace Example
|
|||
|
||||
try
|
||||
{
|
||||
// 3. Bildschirmzeit synchronisieren
|
||||
SyncResponse result = apiInstance.apiV1ScreentimeSyncPost(body);
|
||||
Debug.WriteLine(result);
|
||||
}
|
||||
|
|
@ -2877,6 +2402,7 @@ api_instance = swagger_client.DefaultApi()
|
|||
body = # SyncRequest | (optional)
|
||||
|
||||
try:
|
||||
# 3. Bildschirmzeit synchronisieren
|
||||
api_response = api_instance.api_v1_screentime_sync_post(body=body)
|
||||
pprint(api_response)
|
||||
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">
|
||||
<div class="pull-left">
|
||||
<h1>apiV1TasksGet</h1>
|
||||
<p></p>
|
||||
<p>1. Alle verfügbaren Aufgaben abrufen (mit optionaler Filterung nach Fach und Alter)</p>
|
||||
</div>
|
||||
<div class="pull-right"></div>
|
||||
<div class="clearfix"></div>
|
||||
|
|
@ -3082,6 +2608,7 @@ public class DefaultApiExample {
|
|||
|
||||
DefaultApi *apiInstance = [[DefaultApi alloc] init];
|
||||
|
||||
// 1. Alle verfügbaren Aufgaben abrufen (mit optionaler Filterung nach Fach und Alter)
|
||||
[apiInstance apiV1TasksGetWith:subject
|
||||
age:age
|
||||
limit:limit
|
||||
|
|
@ -3140,6 +2667,7 @@ namespace Example
|
|||
|
||||
try
|
||||
{
|
||||
// 1. Alle verfügbaren Aufgaben abrufen (mit optionaler Filterung nach Fach und Alter)
|
||||
apiInstance.apiV1TasksGet(subject, age, limit);
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
@ -3204,6 +2732,7 @@ age = # | (optional)
|
|||
limit = # | (optional)
|
||||
|
||||
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)
|
||||
except ApiException as e:
|
||||
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">
|
||||
<div class="pull-left">
|
||||
<h1>apiV1TasksVerifyPost</h1>
|
||||
<p></p>
|
||||
<p>2. Antwort auf eine Aufgabe überprüfen</p>
|
||||
</div>
|
||||
<div class="pull-right"></div>
|
||||
<div class="clearfix"></div>
|
||||
|
|
@ -3421,6 +2950,7 @@ TaskVerifyRequest *body = ; // (optional)
|
|||
|
||||
DefaultApi *apiInstance = [[DefaultApi alloc] init];
|
||||
|
||||
// 2. Antwort auf eine Aufgabe überprüfen
|
||||
[apiInstance apiV1TasksVerifyPostWith:body
|
||||
completionHandler: ^(TaskVerifyResponse output, NSError* error) {
|
||||
if (output) {
|
||||
|
|
@ -3476,6 +3006,7 @@ namespace Example
|
|||
|
||||
try
|
||||
{
|
||||
// 2. Antwort auf eine Aufgabe überprüfen
|
||||
TaskVerifyResponse result = apiInstance.apiV1TasksVerifyPost(body);
|
||||
Debug.WriteLine(result);
|
||||
}
|
||||
|
|
@ -3537,6 +3068,7 @@ api_instance = swagger_client.DefaultApi()
|
|||
body = # TaskVerifyRequest | (optional)
|
||||
|
||||
try:
|
||||
# 2. Antwort auf eine Aufgabe überprüfen
|
||||
api_response = api_instance.api_v1_tasks_verify_post(body=body)
|
||||
pprint(api_response)
|
||||
except ApiException as e:
|
||||
|
|
|
|||
|
|
@ -1,14 +1,50 @@
|
|||
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 kotlinx.coroutines.Dispatchers
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
|
||||
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 {
|
||||
fun init(config: ApplicationConfig) {
|
||||
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
|
||||
|
||||
|
|
@ -45,3 +45,51 @@ data class TaskVerifyResponse(
|
|||
val earnedMinutes: Int,
|
||||
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
|
||||
|
||||
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 com.oliver.kidio.backend.ImportedTask
|
||||
import com.oliver.kidio.backend.Task
|
||||
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 {
|
||||
suspend fun getAllTasks(): List<Task> = getTasks(limit = 100)
|
||||
|
||||
suspend fun getUserById(userId: String): User?
|
||||
|
||||
suspend fun findById(userId: String): User?
|
||||
|
||||
suspend fun getTasks(
|
||||
subject: String? = null,
|
||||
age: Int? = null,
|
||||
limit: Int = 10,
|
||||
): List<Task>
|
||||
|
||||
// Mit suspend markiert, damit es zur DatabaseFactory.dbQuery Architektur passt
|
||||
suspend fun getExistingTaskIds(externalSource: String = "zum.de"): Set<String>
|
||||
|
||||
suspend fun upsertTask(task: ImportedTask): Boolean
|
||||
|
|
@ -39,3 +49,331 @@ interface KidioRepository {
|
|||
|
||||
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
|
||||
|
||||
import com.oliver.kidio.backend.data.tables.TasksTable
|
||||
import com.oliver.kidio.backend.domain.models.ImportedTask
|
||||
import com.oliver.kidio.backend.ImportedTask
|
||||
import com.oliver.kidio.backend.data.database.TasksTable
|
||||
import com.oliver.kidio.backend.domain.repository.ExposedKidioRepository
|
||||
import com.oliver.kidio.backend.domain.repository.KidioRepository
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
|
@ -21,9 +21,12 @@ object H5PIngestionCli {
|
|||
limit: Int = 50,
|
||||
): Int =
|
||||
runBlocking {
|
||||
// Bereits vorhandene Task-IDs aus der Datenbank auslesen
|
||||
val existingIds = repository.getExistingTaskIds("zum.de")
|
||||
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 =
|
||||
client.fetchLiveOerItems(
|
||||
subject = subject,
|
||||
|
|
@ -31,92 +34,65 @@ object H5PIngestionCli {
|
|||
existingIds = existingIds,
|
||||
)
|
||||
|
||||
var importedCount = 0
|
||||
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.")
|
||||
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
|
||||
fun main(args: Array<String>) {
|
||||
println("=== Kidio H5P/ZUM Ingestion Tool ===")
|
||||
|
|
|
|||
|
|
@ -4,6 +4,17 @@ import kotlinx.serialization.json.*
|
|||
import java.io.InputStream
|
||||
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 {
|
||||
private val json =
|
||||
Json {
|
||||
|
|
@ -68,9 +79,9 @@ object H5PParser {
|
|||
return null
|
||||
}
|
||||
|
||||
var library = DEFAULT_LIBRARY
|
||||
var title = DEFAULT_TITLE
|
||||
var license = DEFAULT_LICENSE
|
||||
var library = "H5P.MultiChoice"
|
||||
var title = "H5P Task"
|
||||
var license = "CC BY-SA 4.0"
|
||||
|
||||
if (h5pJsonString != null) {
|
||||
try {
|
||||
|
|
@ -78,8 +89,7 @@ object H5PParser {
|
|||
library = h5pTree["mainLibrary"]?.jsonPrimitive?.contentOrNull ?: library
|
||||
title = h5pTree["title"]?.jsonPrimitive?.contentOrNull ?: title
|
||||
license = h5pTree["license"]?.jsonPrimitive?.contentOrNull ?: license
|
||||
} catch (e: Exception) {
|
||||
println("WARNUNG: h5p.json konnte nicht gelesen werden. Nutze Fallback-Werte. Fehler: ${e.message}")
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,10 +108,10 @@ object H5PParser {
|
|||
*/
|
||||
fun parseContentJson(
|
||||
contentJson: String,
|
||||
library: String = DEFAULT_LIBRARY,
|
||||
library: String = "H5P.MultiChoice",
|
||||
taskId: String = "task-1",
|
||||
title: String = DEFAULT_TITLE,
|
||||
license: String = DEFAULT_LICENSE,
|
||||
title: String = "H5P Task",
|
||||
license: String = "CC BY-SA 4.0",
|
||||
sourceUrl: String? = null,
|
||||
): ParsedH5PTask? {
|
||||
val element =
|
||||
|
|
@ -123,10 +133,10 @@ object H5PParser {
|
|||
*/
|
||||
fun parseAllTasks(
|
||||
contentJson: String,
|
||||
library: String = DEFAULT_LIBRARY,
|
||||
library: String = "H5P.MultiChoice",
|
||||
taskId: String = "task-1",
|
||||
title: String = DEFAULT_TITLE,
|
||||
license: String = DEFAULT_LICENSE,
|
||||
title: String = "H5P Task",
|
||||
license: String = "CC BY-SA 4.0",
|
||||
sourceUrl: String? = null,
|
||||
): List<ParsedH5PTask> {
|
||||
val element =
|
||||
|
|
@ -138,46 +148,40 @@ object H5PParser {
|
|||
|
||||
if (library.contains("QuestionSet", ignoreCase = true) && element.containsKey("questions")) {
|
||||
val questionsArray = element["questions"]?.jsonArray ?: return emptyList()
|
||||
val results = mutableListOf<ParsedH5PTask>()
|
||||
|
||||
return questionsArray.mapIndexedNotNull { idx, qElement ->
|
||||
parseQuestionSetItem(qElement, idx, taskId, title, license, sourceUrl)
|
||||
for ((idx, qElement) in questionsArray.withIndex()) {
|
||||
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)
|
||||
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(
|
||||
element: JsonObject,
|
||||
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.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(
|
||||
private val httpClient: HttpClient =
|
||||
HttpClient.newBuilder()
|
||||
|
|
@ -102,7 +113,52 @@ class ZumApiClient(
|
|||
|
||||
for (page in 0 until maxPages) {
|
||||
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()) {
|
||||
|
|
@ -114,78 +170,6 @@ class ZumApiClient(
|
|||
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(
|
||||
appId: String,
|
||||
fallbackSubject: String?,
|
||||
|
|
@ -232,7 +216,7 @@ class ZumApiClient(
|
|||
val rawLicense = metadata?.get("license")?.jsonPrimitive?.contentOrNull
|
||||
val license =
|
||||
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
|
||||
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(
|
||||
subject: String? = null,
|
||||
limit: Int = 20,
|
||||
): List<ZumOerItem> {
|
||||
val allItems: List<ZumOerItem> =
|
||||
try {
|
||||
val inputStream = this::class.java.getResourceAsStream("/fallback_oer_items.json")
|
||||
if (inputStream != null) {
|
||||
val jsonText = inputStream.bufferedReader().use { it.readText() }
|
||||
json.decodeFromString(jsonText)
|
||||
} else {
|
||||
println("WARNUNG: fallback_oer_items.json wurde nicht gefunden!")
|
||||
emptyList()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("WARNUNG: Fehler beim Laden der Fallback-Daten: ${e.message}")
|
||||
emptyList()
|
||||
}
|
||||
val allItems =
|
||||
listOf(
|
||||
ZumOerItem(
|
||||
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}
|
||||
]
|
||||
}
|
||||
""".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 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
|
||||
|
||||
import com.oliver.kidio.backend.*
|
||||
import com.oliver.kidio.backend.domain.repository.KidioRepository
|
||||
import com.oliver.kidio.backend.routing.*
|
||||
import com.oliver.kidio.backend.security.JwtService
|
||||
import io.ktor.http.HttpStatusCode
|
||||
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.request.*
|
||||
import io.ktor.server.response.*
|
||||
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(
|
||||
repository: KidioRepository,
|
||||
|
|
@ -15,13 +41,195 @@ fun Application.configureRouting(
|
|||
routing {
|
||||
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") {
|
||||
call.respond(mapOf("status" to "OK", "message" to "Kidio Backend läuft!"))
|
||||
}
|
||||
|
||||
authRoutes(repository, jwtService)
|
||||
taskRoutes(repository)
|
||||
screentimeRoutes(repository)
|
||||
parentRoutes(repository)
|
||||
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,
|
||||
),
|
||||
)
|
||||
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.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.plugins.configureRouting
|
||||
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.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
|
||||
|
|
@ -36,41 +30,6 @@ class ServerTest {
|
|||
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()
|
||||
|
|
@ -230,41 +189,6 @@ class ServerTest {
|
|||
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()
|
||||
|
|
@ -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