- add verify pin
- restructuring of the backend architecture
This commit is contained in:
parent
c8032cbf62
commit
b3a01a4f16
500
docs/index.html
500
docs/index.html
|
|
@ -766,6 +766,21 @@
|
|||
"earnedMinutes" : { },
|
||||
"message" : { }
|
||||
}
|
||||
};
|
||||
defs["VerifyPinRequest"] = {
|
||||
"title" : "VerifyPinRequest",
|
||||
"required" : [ "pin" ],
|
||||
"properties" : {
|
||||
"pin" : { }
|
||||
}
|
||||
};
|
||||
defs["VerifyPinResponse"] = {
|
||||
"title" : "VerifyPinResponse",
|
||||
"required" : [ "message", "success" ],
|
||||
"properties" : {
|
||||
"success" : { },
|
||||
"message" : { }
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -793,6 +808,9 @@
|
|||
<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>
|
||||
|
|
@ -1630,7 +1648,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>Health-Check</p>
|
||||
<p></p>
|
||||
</div>
|
||||
<div class="pull-right"></div>
|
||||
<div class="clearfix"></div>
|
||||
|
|
@ -1710,7 +1728,6 @@ public class DefaultApiExample {
|
|||
<pre class="prettyprint"><code class="language-cpp">
|
||||
DefaultApi *apiInstance = [[DefaultApi alloc] init];
|
||||
|
||||
// Health-Check
|
||||
[apiInstance apiV1HealthGetWithCompletionHandler:
|
||||
^(NSError* error) {
|
||||
if (error) {
|
||||
|
|
@ -1756,7 +1773,6 @@ namespace Example
|
|||
|
||||
try
|
||||
{
|
||||
// Health-Check
|
||||
apiInstance.apiV1HealthGet();
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
@ -1809,7 +1825,6 @@ 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>
|
||||
|
|
@ -1876,6 +1891,468 @@ 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">
|
||||
|
|
@ -2194,7 +2671,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>3. Bildschirmzeit synchronisieren</p>
|
||||
<p></p>
|
||||
</div>
|
||||
<div class="pull-right"></div>
|
||||
<div class="clearfix"></div>
|
||||
|
|
@ -2284,7 +2761,6 @@ SyncRequest *body = ; // (optional)
|
|||
|
||||
DefaultApi *apiInstance = [[DefaultApi alloc] init];
|
||||
|
||||
// 3. Bildschirmzeit synchronisieren
|
||||
[apiInstance apiV1ScreentimeSyncPostWith:body
|
||||
completionHandler: ^(SyncResponse output, NSError* error) {
|
||||
if (output) {
|
||||
|
|
@ -2340,7 +2816,6 @@ namespace Example
|
|||
|
||||
try
|
||||
{
|
||||
// 3. Bildschirmzeit synchronisieren
|
||||
SyncResponse result = apiInstance.apiV1ScreentimeSyncPost(body);
|
||||
Debug.WriteLine(result);
|
||||
}
|
||||
|
|
@ -2402,7 +2877,6 @@ 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:
|
||||
|
|
@ -2515,7 +2989,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>1. Alle verfügbaren Aufgaben abrufen (mit optionaler Filterung nach Fach und Alter)</p>
|
||||
<p></p>
|
||||
</div>
|
||||
<div class="pull-right"></div>
|
||||
<div class="clearfix"></div>
|
||||
|
|
@ -2608,7 +3082,6 @@ 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
|
||||
|
|
@ -2667,7 +3140,6 @@ namespace Example
|
|||
|
||||
try
|
||||
{
|
||||
// 1. Alle verfügbaren Aufgaben abrufen (mit optionaler Filterung nach Fach und Alter)
|
||||
apiInstance.apiV1TasksGet(subject, age, limit);
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
@ -2732,7 +3204,6 @@ 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>
|
||||
|
|
@ -2860,7 +3331,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>2. Antwort auf eine Aufgabe überprüfen</p>
|
||||
<p></p>
|
||||
</div>
|
||||
<div class="pull-right"></div>
|
||||
<div class="clearfix"></div>
|
||||
|
|
@ -2950,7 +3421,6 @@ 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) {
|
||||
|
|
@ -3006,7 +3476,6 @@ namespace Example
|
|||
|
||||
try
|
||||
{
|
||||
// 2. Antwort auf eine Aufgabe überprüfen
|
||||
TaskVerifyResponse result = apiInstance.apiV1TasksVerifyPost(body);
|
||||
Debug.WriteLine(result);
|
||||
}
|
||||
|
|
@ -3068,7 +3537,6 @@ 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,50 +1,14 @@
|
|||
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"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package com.oliver.kidio.backend.data.tables
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
|
||||
object ScreentimeTable : Table("user_screentime") {
|
||||
val userId = varchar("user_id", 50)
|
||||
val remainingSeconds = integer("remaining_seconds")
|
||||
|
||||
override val primaryKey = PrimaryKey(userId)
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.oliver.kidio.backend.data.tables
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
|
||||
object TasksTable : Table("tasks") {
|
||||
val id = varchar("id", 64)
|
||||
val externalSource = varchar("external_source", 32).default("local")
|
||||
val sourceUrl = text("source_url").nullable()
|
||||
val license = varchar("license", 100).default("CC BY-SA 4.0")
|
||||
val subject = varchar("subject", 64).default("Mathematik")
|
||||
val minAge = integer("min_age").default(6)
|
||||
val maxAge = integer("max_age").default(12)
|
||||
val taskType = varchar("task_type", 32).default("MULTIPLE_CHOICE")
|
||||
val question = text("question")
|
||||
val options = text("options")
|
||||
val correctAnswer = varchar("correct_answer", 255)
|
||||
val rewardMinutes = integer("reward_minutes").default(5)
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
init {
|
||||
index("idx_tasks_filter", false, subject, minAge, maxAge)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.oliver.kidio.backend.data.tables
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
|
||||
object UserTable : Table("users") {
|
||||
val id = varchar("id", 36)
|
||||
val email = varchar("email", 255).uniqueIndex()
|
||||
val passwordHash = varchar("password_hash", 255)
|
||||
val familyName = varchar("family_name", 100).default("Familie")
|
||||
val childAge = integer("child_age").default(8)
|
||||
val parentPin = varchar("parent_pin", 4).default("1234")
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.oliver.kidio.backend.domain.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class RegisterRequest(
|
||||
val email: String,
|
||||
val passwordPlain: String,
|
||||
val familyName: String,
|
||||
val childAge: Int,
|
||||
val parentPin: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LoginRequest(
|
||||
val email: String,
|
||||
val passwordPlain: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AuthResponse(
|
||||
val token: String,
|
||||
val userId: String,
|
||||
val familyName: String,
|
||||
val childAge: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VerifyPinRequest(
|
||||
val pin: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VerifyPinResponse(
|
||||
val success: Boolean,
|
||||
val message: String,
|
||||
)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.oliver.kidio.backend
|
||||
package com.oliver.kidio.backend.domain.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
|
@ -45,51 +45,3 @@ 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,
|
||||
)
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.oliver.kidio.backend.domain.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
// Interne Backend-Klasse (Domain Model).
|
||||
data class User(
|
||||
val id: String,
|
||||
val email: String,
|
||||
val passwordHash: String,
|
||||
val familyName: String,
|
||||
val childAge: Int,
|
||||
val parentPin: String,
|
||||
)
|
||||
|
||||
// Datenabgleich für die Bildschirmzeit (Sync)
|
||||
@Serializable
|
||||
data class SyncRequest(
|
||||
val userId: String,
|
||||
val usedSecondsSinceLastSync: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SyncResponse(
|
||||
val remainingSeconds: Int,
|
||||
val isBlocked: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
package com.oliver.kidio.backend.domain.repository
|
||||
|
||||
import com.oliver.kidio.backend.data.database.DatabaseFactory
|
||||
import com.oliver.kidio.backend.data.database.RedisFactory
|
||||
import com.oliver.kidio.backend.data.tables.ScreentimeTable
|
||||
import com.oliver.kidio.backend.data.tables.TasksTable
|
||||
import com.oliver.kidio.backend.data.tables.UserTable
|
||||
import com.oliver.kidio.backend.domain.models.ImportedTask
|
||||
import com.oliver.kidio.backend.domain.models.Task
|
||||
import com.oliver.kidio.backend.domain.models.User
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.mindrot.jbcrypt.BCrypt
|
||||
|
||||
class ExposedKidioRepository : KidioRepository {
|
||||
override suspend fun getTasks(
|
||||
subject: String?,
|
||||
age: Int?,
|
||||
limit: Int,
|
||||
): List<Task> =
|
||||
DatabaseFactory.dbQuery {
|
||||
var query = TasksTable.selectAll()
|
||||
if (!subject.isNullOrBlank()) {
|
||||
query = query.where { TasksTable.subject.lowerCase() eq subject.trim().lowercase() }
|
||||
}
|
||||
if (age != null) {
|
||||
query = query.andWhere { (TasksTable.minAge lessEq age) and (TasksTable.maxAge greaterEq age) }
|
||||
}
|
||||
val safeLimit = if (limit <= 0) 10 else limit
|
||||
query.limit(safeLimit).map {
|
||||
Task(
|
||||
id = it[TasksTable.id],
|
||||
subject = it[TasksTable.subject],
|
||||
minAge = it[TasksTable.minAge],
|
||||
maxAge = it[TasksTable.maxAge],
|
||||
question = it[TasksTable.question],
|
||||
options = it[TasksTable.options].split(",").map { opt -> opt.trim() },
|
||||
rewardMinutes = it[TasksTable.rewardMinutes],
|
||||
license = it[TasksTable.license],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getUserById(userId: String): User? =
|
||||
DatabaseFactory.dbQuery {
|
||||
UserTable.selectAll()
|
||||
.where { UserTable.id eq userId }
|
||||
.map {
|
||||
User(
|
||||
id = it[UserTable.id],
|
||||
email = it[UserTable.email],
|
||||
passwordHash = it[UserTable.passwordHash],
|
||||
familyName = it[UserTable.familyName],
|
||||
childAge = it[UserTable.childAge],
|
||||
parentPin = it[UserTable.parentPin],
|
||||
)
|
||||
}
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun getAllTasks(): List<Task> = getTasks(limit = 100)
|
||||
|
||||
override suspend fun getExistingTaskIds(externalSource: String): Set<String> =
|
||||
DatabaseFactory.dbQuery {
|
||||
TasksTable
|
||||
.select(TasksTable.id)
|
||||
.where { TasksTable.externalSource eq externalSource }
|
||||
.map { it[TasksTable.id] }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
override suspend fun upsertTask(task: ImportedTask): Boolean =
|
||||
DatabaseFactory.dbQuery {
|
||||
try {
|
||||
val updated =
|
||||
TasksTable.update({ TasksTable.id eq task.id }) {
|
||||
it[externalSource] = task.externalSource
|
||||
it[sourceUrl] = task.sourceUrl
|
||||
it[license] = task.license
|
||||
it[subject] = task.subject
|
||||
it[minAge] = task.minAge
|
||||
it[maxAge] = task.maxAge
|
||||
it[taskType] = task.taskType
|
||||
it[question] = task.question
|
||||
it[options] = task.options.joinToString(",")
|
||||
it[correctAnswer] = task.correctAnswer
|
||||
it[rewardMinutes] = task.rewardMinutes
|
||||
}
|
||||
if (updated == 0) {
|
||||
TasksTable.insert {
|
||||
it[id] = task.id
|
||||
it[externalSource] = task.externalSource
|
||||
it[sourceUrl] = task.sourceUrl
|
||||
it[license] = task.license
|
||||
it[subject] = task.subject
|
||||
it[minAge] = task.minAge
|
||||
it[maxAge] = task.maxAge
|
||||
it[taskType] = task.taskType
|
||||
it[question] = task.question
|
||||
it[options] = task.options.joinToString(",")
|
||||
it[correctAnswer] = task.correctAnswer
|
||||
it[rewardMinutes] = task.rewardMinutes
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
println("FEHLER beim Upsert von Task ${task.id}: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun registerUser(user: User): Boolean =
|
||||
DatabaseFactory.dbQuery {
|
||||
try {
|
||||
UserTable.insert {
|
||||
it[id] = user.id
|
||||
it[email] = user.email
|
||||
it[passwordHash] = user.passwordHash
|
||||
it[familyName] = user.familyName
|
||||
it[childAge] = user.childAge
|
||||
it[parentPin] = user.parentPin
|
||||
}
|
||||
ScreentimeTable.insert {
|
||||
it[userId] = user.id
|
||||
it[remainingSeconds] = 1800 // Startguthaben: 30 Minuten
|
||||
}
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
println("FEHLER bei Registrierung: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>? =
|
||||
DatabaseFactory.dbQuery {
|
||||
TasksTable.selectAll()
|
||||
.where { TasksTable.id eq taskId }
|
||||
.map { it[TasksTable.correctAnswer] to it[TasksTable.rewardMinutes] }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun getScreentime(userId: String): Int {
|
||||
val redisKey = "screentime:$userId"
|
||||
|
||||
try {
|
||||
RedisFactory.getResource().use { jedis ->
|
||||
val cachedSeconds = jedis.get(redisKey)
|
||||
if (cachedSeconds != null) {
|
||||
println("DEBUG: Bildschirmzeit für $userId aus REDIS geladen: $cachedSeconds Sek")
|
||||
return cachedSeconds.toInt()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("WARNUNG: Konnte nicht aus Redis lesen (Postgres-Fallback genutzt): ${e.message}")
|
||||
}
|
||||
|
||||
val dbSeconds =
|
||||
DatabaseFactory.dbQuery {
|
||||
ScreentimeTable.selectAll()
|
||||
.where { ScreentimeTable.userId eq userId }
|
||||
.map { it[ScreentimeTable.remainingSeconds] }
|
||||
.singleOrNull() ?: 1800
|
||||
}
|
||||
|
||||
try {
|
||||
RedisFactory.getResource().use { jedis ->
|
||||
jedis.setex(redisKey, 3600, dbSeconds.toString())
|
||||
println("DEBUG: Bildschirmzeit für $userId in REDIS gecacht.")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("WARNUNG: Konnte Wert nicht in Redis speichern: ${e.message}")
|
||||
}
|
||||
return dbSeconds
|
||||
}
|
||||
|
||||
override suspend fun updateScreentime(
|
||||
userId: String,
|
||||
seconds: Int,
|
||||
) {
|
||||
DatabaseFactory.dbQuery {
|
||||
val rowsUpdated =
|
||||
ScreentimeTable.update({ ScreentimeTable.userId eq userId }) {
|
||||
it[remainingSeconds] = seconds
|
||||
}
|
||||
if (rowsUpdated == 0) {
|
||||
ScreentimeTable.insert {
|
||||
it[ScreentimeTable.userId] = userId
|
||||
it[ScreentimeTable.remainingSeconds] = seconds
|
||||
}
|
||||
}
|
||||
}
|
||||
val redisKey = "screentime:$userId"
|
||||
try {
|
||||
RedisFactory.getResource().use { jedis ->
|
||||
jedis.setex(redisKey, 3600, seconds.toString())
|
||||
println("DEBUG: Bildschirmzeit für $userId in REDIS aktualisiert auf $seconds Sek")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("WARNUNG: Konnte Redis nicht aktualisieren: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun findByEmail(email: String): User? =
|
||||
DatabaseFactory.dbQuery {
|
||||
UserTable.selectAll()
|
||||
.where { UserTable.email eq email }
|
||||
.map {
|
||||
User(
|
||||
id = it[UserTable.id],
|
||||
email = it[UserTable.email],
|
||||
passwordHash = it[UserTable.passwordHash],
|
||||
familyName = it[UserTable.familyName],
|
||||
childAge = it[UserTable.childAge],
|
||||
parentPin = it[UserTable.parentPin],
|
||||
)
|
||||
}
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findById(userId: String): User? =
|
||||
DatabaseFactory.dbQuery {
|
||||
UserTable.selectAll()
|
||||
.where { UserTable.id eq userId }
|
||||
.map {
|
||||
User(
|
||||
id = it[UserTable.id],
|
||||
email = it[UserTable.email],
|
||||
passwordHash = it[UserTable.passwordHash],
|
||||
familyName = it[UserTable.familyName],
|
||||
childAge = it[UserTable.childAge],
|
||||
parentPin = it[UserTable.parentPin],
|
||||
)
|
||||
}
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun verifyPassword(
|
||||
password: String,
|
||||
passwordHash: String,
|
||||
): Boolean {
|
||||
return BCrypt.checkpw(password, passwordHash)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
package com.oliver.kidio.backend.domain.repository
|
||||
|
||||
import com.oliver.kidio.backend.domain.models.ImportedTask
|
||||
import com.oliver.kidio.backend.domain.models.Task
|
||||
import com.oliver.kidio.backend.domain.models.User
|
||||
import org.mindrot.jbcrypt.BCrypt
|
||||
|
||||
class InMemoryKidioRepository : KidioRepository {
|
||||
private val tasks =
|
||||
mutableListOf(
|
||||
ImportedTask(
|
||||
id = "t1",
|
||||
externalSource = "manual",
|
||||
subject = "Mathematik",
|
||||
minAge = 6,
|
||||
maxAge = 10,
|
||||
taskType = "MULTIPLE_CHOICE",
|
||||
question = "Was ist 12 + 15?",
|
||||
options = listOf("25", "27", "30", "22"),
|
||||
correctAnswer = "27",
|
||||
rewardMinutes = 10,
|
||||
license = "CC BY-SA 4.0",
|
||||
sourceUrl = "local",
|
||||
),
|
||||
ImportedTask(
|
||||
id = "t2",
|
||||
externalSource = "manual",
|
||||
subject = "Mathematik",
|
||||
minAge = 7,
|
||||
maxAge = 12,
|
||||
taskType = "MULTIPLE_CHOICE",
|
||||
question = "Was ist 8 x 7?",
|
||||
options = listOf("54", "56", "64", "48"),
|
||||
correctAnswer = "56",
|
||||
rewardMinutes = 15,
|
||||
license = "CC BY-SA 4.0",
|
||||
sourceUrl = "local",
|
||||
),
|
||||
)
|
||||
private val screentimes =
|
||||
mutableMapOf(
|
||||
"default_user" to 1800,
|
||||
)
|
||||
private val users = mutableListOf<User>()
|
||||
|
||||
override suspend fun getAllTasks(): List<Task> = getTasks(limit = 100)
|
||||
|
||||
override suspend fun getTasks(
|
||||
subject: String?,
|
||||
age: Int?,
|
||||
limit: Int,
|
||||
): List<Task> {
|
||||
val safeLimit = if (limit <= 0) 10 else limit
|
||||
return tasks.filter { task ->
|
||||
val matchSubject = subject.isNullOrBlank() || task.subject.equals(subject.trim(), ignoreCase = true)
|
||||
val matchAge = age == null || (task.minAge <= age && task.maxAge >= age)
|
||||
matchSubject && matchAge
|
||||
}.take(safeLimit).map {
|
||||
Task(
|
||||
id = it.id,
|
||||
subject = it.subject,
|
||||
minAge = it.minAge,
|
||||
maxAge = it.maxAge,
|
||||
question = it.question,
|
||||
options = it.options,
|
||||
rewardMinutes = it.rewardMinutes,
|
||||
license = it.license,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getUserById(userId: String): User? {
|
||||
return users.find { it.id == userId }
|
||||
}
|
||||
|
||||
override suspend fun getExistingTaskIds(externalSource: String): Set<String> {
|
||||
return tasks
|
||||
.filter { it.externalSource == externalSource }
|
||||
.map { it.id }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
override suspend fun upsertTask(task: ImportedTask): Boolean {
|
||||
tasks.removeIf { it.id == task.id }
|
||||
tasks.add(task)
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun getTaskAnswerAndReward(taskId: String): Pair<String, Int>? {
|
||||
val task = tasks.find { it.id == taskId } ?: return null
|
||||
return task.correctAnswer to task.rewardMinutes
|
||||
}
|
||||
|
||||
override suspend fun getScreentime(userId: String): Int = screentimes[userId] ?: 1800
|
||||
|
||||
override suspend fun updateScreentime(
|
||||
userId: String,
|
||||
seconds: Int,
|
||||
) {
|
||||
screentimes[userId] = seconds
|
||||
}
|
||||
|
||||
override suspend fun findByEmail(email: String): User? {
|
||||
return users.find { it.email == email }
|
||||
}
|
||||
|
||||
override suspend fun findById(userId: String): User? {
|
||||
return users.find { it.id == userId }
|
||||
}
|
||||
|
||||
override suspend fun verifyPassword(
|
||||
password: String,
|
||||
passwordHash: String,
|
||||
): Boolean {
|
||||
return BCrypt.checkpw(password, passwordHash)
|
||||
}
|
||||
|
||||
override suspend fun registerUser(user: User): Boolean {
|
||||
users.add(user)
|
||||
screentimes[user.id] = 1800
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +1,22 @@
|
|||
package com.oliver.kidio.backend.domain.repository
|
||||
|
||||
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
|
||||
import com.oliver.kidio.backend.domain.models.ImportedTask
|
||||
import com.oliver.kidio.backend.domain.models.Task
|
||||
import com.oliver.kidio.backend.domain.models.User
|
||||
|
||||
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
|
||||
|
|
@ -49,331 +39,3 @@ 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.ImportedTask
|
||||
import com.oliver.kidio.backend.data.database.TasksTable
|
||||
import com.oliver.kidio.backend.data.tables.TasksTable
|
||||
import com.oliver.kidio.backend.domain.models.ImportedTask
|
||||
import com.oliver.kidio.backend.domain.repository.ExposedKidioRepository
|
||||
import com.oliver.kidio.backend.domain.repository.KidioRepository
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
|
|
|||
|
|
@ -1,38 +1,12 @@
|
|||
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,
|
||||
|
|
@ -41,195 +15,13 @@ 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!"))
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
authRoutes(repository, jwtService)
|
||||
taskRoutes(repository)
|
||||
screentimeRoutes(repository)
|
||||
parentRoutes(repository)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
package com.oliver.kidio.backend.routing
|
||||
|
||||
import com.oliver.kidio.backend.domain.models.AuthResponse
|
||||
import com.oliver.kidio.backend.domain.models.LoginRequest
|
||||
import com.oliver.kidio.backend.domain.models.RegisterRequest
|
||||
import com.oliver.kidio.backend.domain.models.User
|
||||
import com.oliver.kidio.backend.domain.repository.KidioRepository
|
||||
import com.oliver.kidio.backend.security.JwtService
|
||||
import com.oliver.kidio.backend.security.isValidPassword
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import org.mindrot.jbcrypt.BCrypt
|
||||
import java.util.UUID
|
||||
|
||||
fun Route.authRoutes(
|
||||
repository: KidioRepository,
|
||||
jwtService: JwtService,
|
||||
) {
|
||||
post("/api/v1/auth/register") {
|
||||
val registerRequest = call.receive<RegisterRequest>()
|
||||
if (registerRequest.email.isBlank() || registerRequest.passwordPlain.isBlank()) {
|
||||
call.respond(HttpStatusCode.BadRequest, "Bitte füllen Sie alle Felder aus")
|
||||
return@post
|
||||
}
|
||||
if (!isValidPassword(registerRequest.passwordPlain)) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
"Das Passwort muss mindestens 8 Zeichen lang sein, " +
|
||||
"mindestens einen Großbuchstaben und ein Sonderzeichen enthalten.",
|
||||
)
|
||||
return@post
|
||||
}
|
||||
if (registerRequest.parentPin.length != 4) {
|
||||
call.respond(HttpStatusCode.BadRequest, "Die Eltern-PIN muss genau 4 Ziffern lang sein")
|
||||
return@post
|
||||
}
|
||||
val existingUser = repository.findByEmail(registerRequest.email)
|
||||
if (existingUser != null) {
|
||||
call.respond(HttpStatusCode.Conflict, "Ein Benutzer mit dieser E-Mail-Adresse existiert bereits")
|
||||
return@post
|
||||
}
|
||||
|
||||
val userId = UUID.randomUUID().toString()
|
||||
val passwordHash = BCrypt.hashpw(registerRequest.passwordPlain, BCrypt.gensalt())
|
||||
|
||||
repository.registerUser(
|
||||
User(
|
||||
userId,
|
||||
registerRequest.email,
|
||||
passwordHash,
|
||||
registerRequest.familyName,
|
||||
registerRequest.childAge,
|
||||
registerRequest.parentPin,
|
||||
),
|
||||
)
|
||||
|
||||
val token = jwtService.generateToken(userId)
|
||||
|
||||
call.respond(
|
||||
HttpStatusCode.Created,
|
||||
mapOf(
|
||||
"token" to token,
|
||||
"userId" to userId,
|
||||
"message" to "Registrierung erfolgreich",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
post("/api/v1/auth/login") {
|
||||
val loginRequest = call.receive<LoginRequest>()
|
||||
if (loginRequest.email.isBlank() || loginRequest.passwordPlain.isBlank()) {
|
||||
call.respond(HttpStatusCode.BadRequest, "Bitte füllen Sie alle Felder aus")
|
||||
return@post
|
||||
}
|
||||
val getUser = repository.findByEmail(loginRequest.email)
|
||||
if (getUser == null || !repository.verifyPassword(loginRequest.passwordPlain, getUser.passwordHash)) {
|
||||
call.respond(HttpStatusCode.Unauthorized, "Ungültige Anmeldedaten")
|
||||
return@post
|
||||
}
|
||||
val token = jwtService.generateToken(getUser.id)
|
||||
call.respond(
|
||||
HttpStatusCode.OK,
|
||||
AuthResponse(
|
||||
token,
|
||||
userId = getUser.id,
|
||||
familyName = getUser.familyName,
|
||||
childAge = getUser.childAge,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.oliver.kidio.backend.routing
|
||||
|
||||
import com.oliver.kidio.backend.domain.models.VerifyPinRequest
|
||||
import com.oliver.kidio.backend.domain.models.VerifyPinResponse
|
||||
import com.oliver.kidio.backend.domain.repository.KidioRepository
|
||||
import com.oliver.kidio.backend.plugins.UserPrincipal
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
fun Route.parentRoutes(repository: KidioRepository) {
|
||||
authenticate("auth-jwt") {
|
||||
post("/api/v1/parent/verify-pin") {
|
||||
val request = call.receive<VerifyPinRequest>()
|
||||
val principal = call.principal<UserPrincipal>()
|
||||
val userId = principal?.userId
|
||||
|
||||
if (userId == null) {
|
||||
call.respond(
|
||||
HttpStatusCode.Unauthorized,
|
||||
mapOf("error" to "Ungültiges oder abgelaufenes Token"),
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
if (request.pin.length != 4 || !request.pin.all { it.isDigit() }) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
VerifyPinResponse(success = false, message = "Die PIN muss aus genau 4 Ziffern bestehen."),
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
val user = repository.findById(userId)
|
||||
if (user == null) {
|
||||
call.respond(
|
||||
HttpStatusCode.NotFound,
|
||||
VerifyPinResponse(success = false, message = "Benutzer nicht gefunden."),
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
if (user.parentPin == request.pin) {
|
||||
call.respond(
|
||||
HttpStatusCode.OK,
|
||||
VerifyPinResponse(success = true, message = "PIN korrekt. Zugriff gewährt."),
|
||||
)
|
||||
} else {
|
||||
call.respond(
|
||||
HttpStatusCode.Unauthorized,
|
||||
VerifyPinResponse(success = false, message = "Falsche PIN."),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.oliver.kidio.backend.routing
|
||||
|
||||
import com.oliver.kidio.backend.domain.models.SyncRequest
|
||||
import com.oliver.kidio.backend.domain.models.SyncResponse
|
||||
import com.oliver.kidio.backend.domain.repository.KidioRepository
|
||||
import com.oliver.kidio.backend.plugins.UserPrincipal
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
fun Route.screentimeRoutes(repository: KidioRepository) {
|
||||
authenticate("auth-jwt") {
|
||||
get("/api/v1/screentime") {
|
||||
val principal = call.principal<UserPrincipal>()
|
||||
val userid =
|
||||
principal?.userId
|
||||
?: run {
|
||||
call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Ungültiger Token"))
|
||||
return@get
|
||||
}
|
||||
val remainingSeconds = repository.getScreentime(userid)
|
||||
call.respond(
|
||||
HttpStatusCode.OK,
|
||||
SyncResponse(
|
||||
remainingSeconds = remainingSeconds,
|
||||
isBlocked = remainingSeconds <= 0,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
post("/api/v1/screentime/sync") {
|
||||
val request = call.receive<SyncRequest>()
|
||||
|
||||
val principal = call.principal<UserPrincipal>()
|
||||
val userId = principal?.userId ?: request.userId
|
||||
|
||||
val currentSeconds = repository.getScreentime(userId)
|
||||
var newSeconds = currentSeconds - request.usedSecondsSinceLastSync
|
||||
if (newSeconds < 0) newSeconds = 0
|
||||
|
||||
repository.updateScreentime(userId, newSeconds)
|
||||
|
||||
call.respond(
|
||||
SyncResponse(
|
||||
remainingSeconds = newSeconds,
|
||||
isBlocked = newSeconds <= 0,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package com.oliver.kidio.backend.routing
|
||||
|
||||
import com.oliver.kidio.backend.domain.models.TaskVerifyRequest
|
||||
import com.oliver.kidio.backend.domain.models.TaskVerifyResponse
|
||||
import com.oliver.kidio.backend.domain.repository.KidioRepository
|
||||
import com.oliver.kidio.backend.plugins.UserPrincipal
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
fun Route.taskRoutes(repository: KidioRepository) {
|
||||
authenticate("auth-jwt") {
|
||||
get("/api/v1/tasks") {
|
||||
val principal = call.principal<UserPrincipal>()
|
||||
val userId = principal?.userId
|
||||
|
||||
val subject = call.request.queryParameters["subject"]
|
||||
|
||||
val queryAge = call.request.queryParameters["age"]?.toIntOrNull()
|
||||
val userAge = userId?.let { repository.getUserById(it)?.childAge }
|
||||
val effectiveAge = queryAge ?: userAge
|
||||
|
||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 10
|
||||
|
||||
val tasks = repository.getTasks(subject = subject, age = effectiveAge, limit = limit)
|
||||
call.respond(tasks)
|
||||
}
|
||||
|
||||
post("/api/v1/tasks/verify") {
|
||||
val request = call.receive<TaskVerifyRequest>()
|
||||
val principal = call.principal<UserPrincipal>()
|
||||
val userId = principal?.userId
|
||||
if (userId == null) {
|
||||
call.respond(
|
||||
HttpStatusCode.Unauthorized,
|
||||
mapOf("error" to "Ungültiges oder abgelaufenes Token"),
|
||||
)
|
||||
return@post
|
||||
}
|
||||
val taskInfo = repository.getTaskAnswerAndReward(request.taskId)
|
||||
if (taskInfo == null) {
|
||||
call.respond(
|
||||
TaskVerifyResponse(
|
||||
isCorrect = false,
|
||||
earnedMinutes = 0,
|
||||
message = "Aufgabe nicht gefunden!",
|
||||
),
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
val (correctAnswer, reward) = taskInfo
|
||||
val isCorrect = request.selectedAnswer.trim() == correctAnswer.trim()
|
||||
|
||||
if (isCorrect) {
|
||||
val currentScreentime = repository.getScreentime(userId)
|
||||
val newSeconds = currentScreentime + (reward * 60)
|
||||
repository.updateScreentime(userId, newSeconds)
|
||||
|
||||
call.respond(
|
||||
TaskVerifyResponse(
|
||||
isCorrect = true,
|
||||
earnedMinutes = reward,
|
||||
message = "Super gemacht! Du hast $reward Minuten gewonnen.",
|
||||
),
|
||||
)
|
||||
} else {
|
||||
call.respond(
|
||||
TaskVerifyResponse(
|
||||
isCorrect = false,
|
||||
earnedMinutes = 0,
|
||||
message = "Schade, das war leider falsch. Versuche es nochmal!",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.oliver.kidio.backend.security
|
||||
|
||||
fun isValidPassword(password: String): Boolean {
|
||||
if (password.length < 8) return false
|
||||
|
||||
val hasUppercase = password.any { it.isUpperCase() }
|
||||
val hasEmoji =
|
||||
password.any { char ->
|
||||
val type = Character.getType(char).toByte()
|
||||
type == Character.SURROGATE ||
|
||||
type == Character.OTHER_SYMBOL ||
|
||||
char.code in 0x1F600..0x1F64F ||
|
||||
char.code in 0x1F300..0x1F5FF ||
|
||||
char.code in 0x1F680..0x1F6FF ||
|
||||
char.code in 0x2600..0x26FF
|
||||
}
|
||||
|
||||
val hasSpecialChar = password.any { !it.isLetterOrDigit() && !hasEmoji }
|
||||
|
||||
return hasUppercase && hasSpecialChar && !hasEmoji
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.oliver.kidio.backend
|
||||
package com.oliver.kidio.backend.com.oliver.kidio.backend.ingestion
|
||||
|
||||
import com.oliver.kidio.backend.domain.repository.InMemoryKidioRepository
|
||||
import com.oliver.kidio.backend.ingestion.H5PIngestionCli
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
package com.oliver.kidio.backend.routing
|
||||
|
||||
import com.oliver.kidio.backend.domain.models.AuthResponse
|
||||
import com.oliver.kidio.backend.domain.repository.InMemoryKidioRepository
|
||||
import com.oliver.kidio.backend.plugins.configureRouting
|
||||
import com.oliver.kidio.backend.plugins.configureSecurity
|
||||
import com.oliver.kidio.backend.security.JwtService
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.testing.testApplication
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation
|
||||
|
||||
class AuthRoutesTest {
|
||||
@Test
|
||||
fun `test registration and login flow`() =
|
||||
testApplication {
|
||||
val jwtService = JwtService()
|
||||
val repository = InMemoryKidioRepository()
|
||||
|
||||
application {
|
||||
install(ServerContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
configureSecurity(jwtService)
|
||||
configureRouting(repository, jwtService)
|
||||
}
|
||||
|
||||
// 1. Registrierung
|
||||
val registerResponse =
|
||||
client.post("/api/v1/auth/register") {
|
||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
setBody(
|
||||
"""
|
||||
{
|
||||
"email": "test@family.de",
|
||||
"passwordPlain": "StrengGeheim1!",
|
||||
"familyName": "Familie Schmidt",
|
||||
"childAge": 8,
|
||||
"parentPin": "4321"
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
assertEquals(HttpStatusCode.Created, registerResponse.status)
|
||||
assertTrue(registerResponse.bodyAsText().contains("token"))
|
||||
|
||||
// 2. Login mit denselben Daten
|
||||
val loginResponse =
|
||||
client.post("/api/v1/auth/login") {
|
||||
header(
|
||||
HttpHeaders.ContentType,
|
||||
ContentType.Application.Json.toString(),
|
||||
)
|
||||
setBody(
|
||||
"""
|
||||
{
|
||||
"email": "test@family.de",
|
||||
"passwordPlain": "StrengGeheim1!"
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
assertEquals(HttpStatusCode.OK, loginResponse.status)
|
||||
val loginJson = Json.decodeFromString<AuthResponse>(loginResponse.bodyAsText())
|
||||
assertNotNull(loginJson.token)
|
||||
assertEquals("Familie Schmidt", loginJson.familyName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test registration rejects invalid passwords`() =
|
||||
testApplication {
|
||||
val jwtService = JwtService()
|
||||
val repository = InMemoryKidioRepository()
|
||||
|
||||
application {
|
||||
install(ServerContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
configureSecurity(jwtService)
|
||||
configureRouting(repository, jwtService)
|
||||
}
|
||||
|
||||
val invalidPasswords =
|
||||
listOf(
|
||||
"Kurz1!",
|
||||
"keingrossbuch1!",
|
||||
"KeinSonder12",
|
||||
"Passwort123!😊",
|
||||
)
|
||||
|
||||
for (password in invalidPasswords) {
|
||||
val response =
|
||||
client.post("/api/v1/auth/register") {
|
||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
setBody(
|
||||
"""
|
||||
{
|
||||
"email": "test_${System.currentTimeMillis()}@family.de",
|
||||
"passwordPlain": "$password",
|
||||
"familyName": "Familie Schmidt",
|
||||
"childAge": 8,
|
||||
"parentPin": "4321"
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
HttpStatusCode.BadRequest,
|
||||
response.status,
|
||||
"Passwort '$password' hätte abgelehnt werden müssen!",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.oliver.kidio.backend.routing
|
||||
|
||||
import com.oliver.kidio.backend.domain.repository.InMemoryKidioRepository
|
||||
import com.oliver.kidio.backend.plugins.configureRouting
|
||||
import com.oliver.kidio.backend.plugins.configureSecurity
|
||||
import com.oliver.kidio.backend.security.JwtService
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.testing.testApplication
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation
|
||||
|
||||
class HealthRouteTest {
|
||||
@Test
|
||||
fun `test health endpoint`() =
|
||||
testApplication {
|
||||
val jwtService = JwtService()
|
||||
val repository = InMemoryKidioRepository()
|
||||
|
||||
application {
|
||||
install(ServerContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
configureSecurity(jwtService)
|
||||
configureRouting(repository, jwtService)
|
||||
}
|
||||
|
||||
val response = client.get("/api/v1/health")
|
||||
assertEquals(HttpStatusCode.OK, response.status)
|
||||
assertTrue(response.bodyAsText().contains("Kidio Backend läuft!"))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package com.oliver.kidio.backend.routing
|
||||
|
||||
import com.oliver.kidio.backend.domain.models.SyncResponse
|
||||
import com.oliver.kidio.backend.domain.repository.InMemoryKidioRepository
|
||||
import com.oliver.kidio.backend.plugins.configureRouting
|
||||
import com.oliver.kidio.backend.plugins.configureSecurity
|
||||
import com.oliver.kidio.backend.security.JwtService
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.testing.testApplication
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation
|
||||
|
||||
class ScreentimeRoutesTest {
|
||||
@Test
|
||||
fun `test get screentime returns current status`() =
|
||||
testApplication {
|
||||
val jwtService = JwtService()
|
||||
val repository = InMemoryKidioRepository()
|
||||
|
||||
application {
|
||||
install(ServerContentNegotiation) { json() }
|
||||
configureSecurity(jwtService)
|
||||
configureRouting(repository, jwtService)
|
||||
}
|
||||
|
||||
val token = jwtService.generateToken("default_user")
|
||||
|
||||
// Ohne Token -> 401 Unauthorized
|
||||
val unauthResponse = client.get("/api/v1/screentime")
|
||||
assertEquals(HttpStatusCode.Unauthorized, unauthResponse.status)
|
||||
|
||||
// Mit Token -> 200 OK
|
||||
val response =
|
||||
client.get("/api/v1/screentime") {
|
||||
header(HttpHeaders.Authorization, "Bearer $token")
|
||||
}
|
||||
assertEquals(HttpStatusCode.OK, response.status)
|
||||
val json = Json.decodeFromString<SyncResponse>(response.bodyAsText())
|
||||
assertEquals(1800, json.remainingSeconds)
|
||||
assertFalse(json.isBlocked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test screentime sync deducts time repeatedly`() =
|
||||
testApplication {
|
||||
val jwtService = JwtService()
|
||||
val repository = InMemoryKidioRepository()
|
||||
|
||||
application {
|
||||
install(ServerContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
configureSecurity(jwtService)
|
||||
configureRouting(repository, jwtService)
|
||||
}
|
||||
|
||||
val token = jwtService.generateToken("default_user")
|
||||
|
||||
// 1. Sync: 200 Sekunden abziehen (1800 -> 1600)
|
||||
val syncResponse1 =
|
||||
client.post("/api/v1/screentime/sync") {
|
||||
header(HttpHeaders.Authorization, "Bearer $token")
|
||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
setBody("""{"userId":"default_user","usedSecondsSinceLastSync":200}""")
|
||||
}
|
||||
assertEquals(HttpStatusCode.OK, syncResponse1.status)
|
||||
val syncJson1 = Json.decodeFromString<SyncResponse>(syncResponse1.bodyAsText())
|
||||
assertEquals(1600, syncJson1.remainingSeconds)
|
||||
assertFalse(syncJson1.isBlocked)
|
||||
|
||||
// 2. Erneuter Sync: Weitere 300 Sekunden abziehen (1600 -> 1300)
|
||||
val syncResponse2 =
|
||||
client.post("/api/v1/screentime/sync") {
|
||||
header(HttpHeaders.Authorization, "Bearer $token")
|
||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
setBody("""{"userId":"default_user","usedSecondsSinceLastSync":300}""")
|
||||
}
|
||||
assertEquals(HttpStatusCode.OK, syncResponse2.status)
|
||||
val syncJson2 = Json.decodeFromString<SyncResponse>(syncResponse2.bodyAsText())
|
||||
assertEquals(1300, syncJson2.remainingSeconds)
|
||||
assertFalse(syncJson2.isBlocked)
|
||||
|
||||
// 3. Sync bis zur Sperre (weitere 1500 Sekunden abziehen -> 0)
|
||||
val syncResponse3 =
|
||||
client.post("/api/v1/screentime/sync") {
|
||||
header(HttpHeaders.Authorization, "Bearer $token")
|
||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
setBody("""{"userId":"default_user","usedSecondsSinceLastSync":1500}""")
|
||||
}
|
||||
assertEquals(HttpStatusCode.OK, syncResponse3.status)
|
||||
val syncJson3 = Json.decodeFromString<SyncResponse>(syncResponse3.bodyAsText())
|
||||
assertEquals(0, syncJson3.remainingSeconds)
|
||||
assertTrue(syncJson3.isBlocked)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
package com.oliver.kidio.backend
|
||||
package com.oliver.kidio.backend.com.oliver.kidio.backend.routing
|
||||
|
||||
import com.oliver.kidio.backend.domain.models.AuthResponse
|
||||
import com.oliver.kidio.backend.domain.models.SyncResponse
|
||||
import com.oliver.kidio.backend.domain.models.Task
|
||||
import com.oliver.kidio.backend.domain.models.TaskVerifyResponse
|
||||
import com.oliver.kidio.backend.domain.repository.InMemoryKidioRepository
|
||||
import com.oliver.kidio.backend.plugins.configureRouting
|
||||
import com.oliver.kidio.backend.plugins.configureSecurity
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package com.oliver.kidio.backend.routing
|
||||
|
||||
import com.oliver.kidio.backend.domain.models.Task
|
||||
import com.oliver.kidio.backend.domain.models.TaskVerifyResponse
|
||||
import com.oliver.kidio.backend.domain.repository.InMemoryKidioRepository
|
||||
import com.oliver.kidio.backend.plugins.configureRouting
|
||||
import com.oliver.kidio.backend.plugins.configureSecurity
|
||||
import com.oliver.kidio.backend.security.JwtService
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.testing.testApplication
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation
|
||||
|
||||
class TaskRoutesTest {
|
||||
@Test
|
||||
fun `test protected endpoints reject unauthorized requests`() =
|
||||
testApplication {
|
||||
val jwtService = JwtService()
|
||||
val repository = InMemoryKidioRepository()
|
||||
|
||||
application {
|
||||
install(ServerContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
configureSecurity(jwtService)
|
||||
configureRouting(repository, jwtService)
|
||||
}
|
||||
|
||||
// Ohne Token muss 401 Unauthorized kommen
|
||||
val tasksResponse = client.get("/api/v1/tasks")
|
||||
assertEquals(HttpStatusCode.Unauthorized, tasksResponse.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test tasks endpoint and verify with jwt`() =
|
||||
testApplication {
|
||||
val jwtService = JwtService()
|
||||
val repository = InMemoryKidioRepository()
|
||||
|
||||
application {
|
||||
install(ServerContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
configureSecurity(jwtService)
|
||||
configureRouting(repository, jwtService)
|
||||
}
|
||||
|
||||
val token = jwtService.generateToken("default_user")
|
||||
|
||||
// 1. Get Tasks mit Token
|
||||
val tasksResponse =
|
||||
client.get("/api/v1/tasks") {
|
||||
header(HttpHeaders.Authorization, "Bearer $token")
|
||||
}
|
||||
assertEquals(HttpStatusCode.OK, tasksResponse.status)
|
||||
val tasksJson = Json.decodeFromString<List<Task>>(tasksResponse.bodyAsText())
|
||||
assertEquals(2, tasksJson.size)
|
||||
assertEquals("t1", tasksJson[0].id)
|
||||
assertEquals("Was ist 12 + 15?", tasksJson[0].question)
|
||||
|
||||
// 2. Filter nach Fach & Alter
|
||||
val filteredResponse =
|
||||
client.get("/api/v1/tasks?subject=Mathematik&age=8") {
|
||||
header(HttpHeaders.Authorization, "Bearer $token")
|
||||
}
|
||||
assertEquals(HttpStatusCode.OK, filteredResponse.status)
|
||||
val filteredJson = Json.decodeFromString<List<Task>>(filteredResponse.bodyAsText())
|
||||
assertEquals(2, filteredJson.size)
|
||||
|
||||
// 3. Filter mit Alter außerhalb des Bereichs
|
||||
val age15Response =
|
||||
client.get("/api/v1/tasks?subject=Mathematik&age=15") {
|
||||
header(HttpHeaders.Authorization, "Bearer $token")
|
||||
}
|
||||
assertEquals(HttpStatusCode.OK, age15Response.status)
|
||||
val age15Json = Json.decodeFromString<List<Task>>(age15Response.bodyAsText())
|
||||
assertEquals(0, age15Json.size)
|
||||
|
||||
// 4. Antwort enthält NIEMALS correctAnswer
|
||||
assertFalse(tasksResponse.bodyAsText().contains("correctAnswer"))
|
||||
assertFalse(tasksResponse.bodyAsText().contains("correct_answer"))
|
||||
|
||||
// 5. Verify Task correct answer
|
||||
val verifyResponse =
|
||||
client.post("/api/v1/tasks/verify") {
|
||||
header(HttpHeaders.Authorization, "Bearer $token")
|
||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
setBody("""{"taskId":"t1","selectedAnswer":"27"}""")
|
||||
}
|
||||
assertEquals(HttpStatusCode.OK, verifyResponse.status)
|
||||
val verifyJson = Json.decodeFromString<TaskVerifyResponse>(verifyResponse.bodyAsText())
|
||||
assertTrue(verifyJson.isCorrect)
|
||||
assertEquals(10, verifyJson.earnedMinutes)
|
||||
|
||||
// 6. Verify Task incorrect answer
|
||||
val verifyWrongResponse =
|
||||
client.post("/api/v1/tasks/verify") {
|
||||
header(HttpHeaders.Authorization, "Bearer $token")
|
||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
setBody("""{"taskId":"t1","selectedAnswer":"99"}""")
|
||||
}
|
||||
assertEquals(HttpStatusCode.OK, verifyWrongResponse.status)
|
||||
val verifyWrongJson = Json.decodeFromString<TaskVerifyResponse>(verifyWrongResponse.bodyAsText())
|
||||
assertFalse(verifyWrongJson.isCorrect)
|
||||
assertEquals(0, verifyWrongJson.earnedMinutes)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.oliver.kidio.backend.security
|
||||
|
||||
import com.auth0.jwt.JWT
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
class JwtServiceTest {
|
||||
@Test
|
||||
fun `test generate token contains correct user id and claims`() {
|
||||
// Wir nutzen ein Dummy-Secret für den Test
|
||||
val service = JwtService(secret = "test-secret-für-unit-tests")
|
||||
val token = service.generateToken("user-123")
|
||||
|
||||
assertNotNull(token)
|
||||
|
||||
// Token decodieren und prüfen, ob unsere Daten korrekt verpackt wurden
|
||||
val decoded = JWT.decode(token)
|
||||
val userIdClaim = decoded.getClaim("userId").asString()
|
||||
|
||||
assertEquals("user-123", userIdClaim)
|
||||
assertEquals("kidio-users", decoded.audience.firstOrNull())
|
||||
assertEquals("https://kidio.oliver.com/", decoded.issuer)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.oliver.kidio.backend.security
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class PasswordUtilsTest {
|
||||
@Test
|
||||
fun `test valid passwords`() {
|
||||
assertTrue(isValidPassword("StrengGeheim1!"))
|
||||
assertTrue(isValidPassword("Kidio2026_Secure"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test password too short`() {
|
||||
assertFalse(isValidPassword("Kurz1!"))
|
||||
assertFalse(isValidPassword("Geheim!"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test missing uppercase`() {
|
||||
assertFalse(isValidPassword("kleingeschrieben1!"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test missing special character`() {
|
||||
assertFalse(isValidPassword("KeinSonderzeichen123"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test password with emojis is rejected`() {
|
||||
assertFalse(isValidPassword("Passwort123!😊"))
|
||||
assertFalse(isValidPassword("🚀Geheim123!"))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue