mirror of
https://codeberg.org/timelimit/timelimit-server.git
synced 2025-10-03 09:49:32 +02:00
Add identity tokens
This commit is contained in:
parent
a86a0abb05
commit
04aa2ce517
18 changed files with 390 additions and 11 deletions
|
@ -169,3 +169,28 @@ If there is no device with the specified ``deviceId``: HTTP status code 409 Conf
|
||||||
If the ``secondPasswordHash`` is invalid: HTTP status code 401 Unauthorized
|
If the ``secondPasswordHash`` is invalid: HTTP status code 401 Unauthorized
|
||||||
|
|
||||||
On success: ``{"ok": true}``
|
On success: ``{"ok": true}``
|
||||||
|
|
||||||
|
## POST /parent/create-identity-token
|
||||||
|
|
||||||
|
Use this to get a identity token.
|
||||||
|
This can be used to inform the server operator about ones user account.
|
||||||
|
|
||||||
|
### request
|
||||||
|
|
||||||
|
see [this JSON schema](../schema/requestidentitytokenrequest.md)
|
||||||
|
|
||||||
|
in case of a device used by a parent with disabled password checks, use ``device`` as ``secondPasswordHash``
|
||||||
|
|
||||||
|
## response
|
||||||
|
|
||||||
|
On a invalid request body: HTTP status code 400 Bad Request
|
||||||
|
|
||||||
|
If the device auth token is invalid: HTTP status code 401 Unauthorized
|
||||||
|
|
||||||
|
If there is no device with the specified ``deviceId``: HTTP status code 409 Conflict
|
||||||
|
|
||||||
|
If the ``secondPasswordHash`` is invalid: HTTP status code 401 Unauthorized
|
||||||
|
|
||||||
|
If the server does not support this request: HTTP status code 404
|
||||||
|
|
||||||
|
On success: ``{"token": "some string"}``; you should not make any assumptions about the token string
|
||||||
|
|
|
@ -26,6 +26,8 @@
|
||||||
|
|
||||||
* [RemoveDeviceRequest](./removedevicerequest.md) – `https://timelimit.io/RemoveDeviceRequest`
|
* [RemoveDeviceRequest](./removedevicerequest.md) – `https://timelimit.io/RemoveDeviceRequest`
|
||||||
|
|
||||||
|
* [RequestIdentityTokenRequest](./requestidentitytokenrequest.md) – `https://timelimit.io/RequestIdentityTokenRequest`
|
||||||
|
|
||||||
* [RequestWithAuthToken](./requestwithauthtoken.md) – `https://timelimit.io/RequestWithAuthToken`
|
* [RequestWithAuthToken](./requestwithauthtoken.md) – `https://timelimit.io/RequestWithAuthToken`
|
||||||
|
|
||||||
* [SendMailLoginCodeRequest](./sendmaillogincoderequest.md) – `https://timelimit.io/SendMailLoginCodeRequest`
|
* [SendMailLoginCodeRequest](./sendmaillogincoderequest.md) – `https://timelimit.io/SendMailLoginCodeRequest`
|
||||||
|
|
31
docs/schema/RequestIdentityTokenRequest.schema.json
Normal file
31
docs/schema/RequestIdentityTokenRequest.schema.json
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"deviceAuthToken": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"parentUserId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"parentPasswordSecondHash": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"purpose": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"purchase"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"deviceAuthToken",
|
||||||
|
"parentPasswordSecondHash",
|
||||||
|
"parentUserId",
|
||||||
|
"purpose"
|
||||||
|
],
|
||||||
|
"definitions": {},
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "RequestIdentityTokenRequest",
|
||||||
|
"$id": "https://timelimit.io/RequestIdentityTokenRequest"
|
||||||
|
}
|
15
docs/schema/requestidentitytokenrequest-definitions.md
Normal file
15
docs/schema/requestidentitytokenrequest-definitions.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# Untitled undefined type in RequestIdentityTokenRequest Schema
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://timelimit.io/RequestIdentityTokenRequest#/definitions
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
|
||||||
|
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :---------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [RequestIdentityTokenRequest.schema.json\*](RequestIdentityTokenRequest.schema.json "open original schema") |
|
||||||
|
|
||||||
|
## definitions Type
|
||||||
|
|
||||||
|
unknown
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Untitled string in RequestIdentityTokenRequest Schema
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://timelimit.io/RequestIdentityTokenRequest#/properties/deviceAuthToken
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
|
||||||
|
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :---------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [RequestIdentityTokenRequest.schema.json\*](RequestIdentityTokenRequest.schema.json "open original schema") |
|
||||||
|
|
||||||
|
## deviceAuthToken Type
|
||||||
|
|
||||||
|
`string`
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Untitled string in RequestIdentityTokenRequest Schema
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://timelimit.io/RequestIdentityTokenRequest#/properties/parentPasswordSecondHash
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
|
||||||
|
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :---------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [RequestIdentityTokenRequest.schema.json\*](RequestIdentityTokenRequest.schema.json "open original schema") |
|
||||||
|
|
||||||
|
## parentPasswordSecondHash Type
|
||||||
|
|
||||||
|
`string`
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Untitled string in RequestIdentityTokenRequest Schema
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://timelimit.io/RequestIdentityTokenRequest#/properties/parentUserId
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
|
||||||
|
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :---------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [RequestIdentityTokenRequest.schema.json\*](RequestIdentityTokenRequest.schema.json "open original schema") |
|
||||||
|
|
||||||
|
## parentUserId Type
|
||||||
|
|
||||||
|
`string`
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Untitled string in RequestIdentityTokenRequest Schema
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://timelimit.io/RequestIdentityTokenRequest#/properties/purpose
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
|
||||||
|
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :---------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [RequestIdentityTokenRequest.schema.json\*](RequestIdentityTokenRequest.schema.json "open original schema") |
|
||||||
|
|
||||||
|
## purpose Type
|
||||||
|
|
||||||
|
`string`
|
||||||
|
|
||||||
|
## purpose Constraints
|
||||||
|
|
||||||
|
**enum**: the value of this property must be equal to one of the following values:
|
||||||
|
|
||||||
|
| Value | Explanation |
|
||||||
|
| :----------- | :---------- |
|
||||||
|
| `"purchase"` | |
|
106
docs/schema/requestidentitytokenrequest.md
Normal file
106
docs/schema/requestidentitytokenrequest.md
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
# RequestIdentityTokenRequest Schema
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://timelimit.io/RequestIdentityTokenRequest
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
|
||||||
|
| :------------------ | :--------- | :------------- | :----------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Can be instantiated | Yes | Unknown status | No | Forbidden | Forbidden | none | [RequestIdentityTokenRequest.schema.json](RequestIdentityTokenRequest.schema.json "open original schema") |
|
||||||
|
|
||||||
|
## RequestIdentityTokenRequest Type
|
||||||
|
|
||||||
|
`object` ([RequestIdentityTokenRequest](requestidentitytokenrequest.md))
|
||||||
|
|
||||||
|
# RequestIdentityTokenRequest Properties
|
||||||
|
|
||||||
|
| Property | Type | Required | Nullable | Defined by |
|
||||||
|
| :---------------------------------------------------- | :------- | :------- | :------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| [deviceAuthToken](#deviceauthtoken) | `string` | Required | cannot be null | [RequestIdentityTokenRequest](requestidentitytokenrequest-properties-deviceauthtoken.md "https://timelimit.io/RequestIdentityTokenRequest#/properties/deviceAuthToken") |
|
||||||
|
| [parentUserId](#parentuserid) | `string` | Required | cannot be null | [RequestIdentityTokenRequest](requestidentitytokenrequest-properties-parentuserid.md "https://timelimit.io/RequestIdentityTokenRequest#/properties/parentUserId") |
|
||||||
|
| [parentPasswordSecondHash](#parentpasswordsecondhash) | `string` | Required | cannot be null | [RequestIdentityTokenRequest](requestidentitytokenrequest-properties-parentpasswordsecondhash.md "https://timelimit.io/RequestIdentityTokenRequest#/properties/parentPasswordSecondHash") |
|
||||||
|
| [purpose](#purpose) | `string` | Required | cannot be null | [RequestIdentityTokenRequest](requestidentitytokenrequest-properties-purpose.md "https://timelimit.io/RequestIdentityTokenRequest#/properties/purpose") |
|
||||||
|
|
||||||
|
## deviceAuthToken
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`deviceAuthToken`
|
||||||
|
|
||||||
|
* is required
|
||||||
|
|
||||||
|
* Type: `string`
|
||||||
|
|
||||||
|
* cannot be null
|
||||||
|
|
||||||
|
* defined in: [RequestIdentityTokenRequest](requestidentitytokenrequest-properties-deviceauthtoken.md "https://timelimit.io/RequestIdentityTokenRequest#/properties/deviceAuthToken")
|
||||||
|
|
||||||
|
### deviceAuthToken Type
|
||||||
|
|
||||||
|
`string`
|
||||||
|
|
||||||
|
## parentUserId
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`parentUserId`
|
||||||
|
|
||||||
|
* is required
|
||||||
|
|
||||||
|
* Type: `string`
|
||||||
|
|
||||||
|
* cannot be null
|
||||||
|
|
||||||
|
* defined in: [RequestIdentityTokenRequest](requestidentitytokenrequest-properties-parentuserid.md "https://timelimit.io/RequestIdentityTokenRequest#/properties/parentUserId")
|
||||||
|
|
||||||
|
### parentUserId Type
|
||||||
|
|
||||||
|
`string`
|
||||||
|
|
||||||
|
## parentPasswordSecondHash
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`parentPasswordSecondHash`
|
||||||
|
|
||||||
|
* is required
|
||||||
|
|
||||||
|
* Type: `string`
|
||||||
|
|
||||||
|
* cannot be null
|
||||||
|
|
||||||
|
* defined in: [RequestIdentityTokenRequest](requestidentitytokenrequest-properties-parentpasswordsecondhash.md "https://timelimit.io/RequestIdentityTokenRequest#/properties/parentPasswordSecondHash")
|
||||||
|
|
||||||
|
### parentPasswordSecondHash Type
|
||||||
|
|
||||||
|
`string`
|
||||||
|
|
||||||
|
## purpose
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`purpose`
|
||||||
|
|
||||||
|
* is required
|
||||||
|
|
||||||
|
* Type: `string`
|
||||||
|
|
||||||
|
* cannot be null
|
||||||
|
|
||||||
|
* defined in: [RequestIdentityTokenRequest](requestidentitytokenrequest-properties-purpose.md "https://timelimit.io/RequestIdentityTokenRequest#/properties/purpose")
|
||||||
|
|
||||||
|
### purpose Type
|
||||||
|
|
||||||
|
`string`
|
||||||
|
|
||||||
|
### purpose Constraints
|
||||||
|
|
||||||
|
**enum**: the value of this property must be equal to one of the following values:
|
||||||
|
|
||||||
|
| Value | Explanation |
|
||||||
|
| :----------- | :---------- |
|
||||||
|
| `"purchase"` | |
|
||||||
|
|
||||||
|
# RequestIdentityTokenRequest Definitions
|
|
@ -54,3 +54,6 @@
|
||||||
- PING_INTERVAL_SEC
|
- PING_INTERVAL_SEC
|
||||||
- ping interval at the websocket in seconds
|
- ping interval at the websocket in seconds
|
||||||
- the default value is ``25``
|
- the default value is ``25``
|
||||||
|
- SIGN_SECRET
|
||||||
|
- used for signing tokens
|
||||||
|
- if not set or set to an empty string, then the features that depend on it are disabled
|
||||||
|
|
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -16,6 +16,7 @@
|
||||||
"email-addresses": "^3.1.0",
|
"email-addresses": "^3.1.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"http-errors": "^1.8.0",
|
"http-errors": "^1.8.0",
|
||||||
|
"jose": "^4.9.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mariadb": "^2.5.2",
|
"mariadb": "^2.5.2",
|
||||||
"nodemailer": "^6.7.2",
|
"nodemailer": "^6.7.2",
|
||||||
|
@ -2333,6 +2334,14 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jose": {
|
||||||
|
"version": "4.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jose/-/jose-4.9.3.tgz",
|
||||||
|
"integrity": "sha512-f8E/z+T3Q0kA9txzH2DKvH/ds2uggcw0m3vVPSB9HrSkrQ7mojjifvS7aR8cw+lQl2Fcmx9npwaHpM/M3GD8UQ==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
|
@ -7071,6 +7080,11 @@
|
||||||
"minimatch": "^3.0.4"
|
"minimatch": "^3.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"jose": {
|
||||||
|
"version": "4.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jose/-/jose-4.9.3.tgz",
|
||||||
|
"integrity": "sha512-f8E/z+T3Q0kA9txzH2DKvH/ds2uggcw0m3vVPSB9HrSkrQ7mojjifvS7aR8cw+lQl2Fcmx9npwaHpM/M3GD8UQ=="
|
||||||
|
},
|
||||||
"js-yaml": {
|
"js-yaml": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
|
|
|
@ -54,6 +54,7 @@
|
||||||
"email-addresses": "^3.1.0",
|
"email-addresses": "^3.1.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"http-errors": "^1.8.0",
|
"http-errors": "^1.8.0",
|
||||||
|
"jose": "^4.9.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mariadb": "^2.5.2",
|
"mariadb": "^2.5.2",
|
||||||
"nodemailer": "^6.7.2",
|
"nodemailer": "^6.7.2",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* server component for the TimeLimit App
|
* server component for the TimeLimit App
|
||||||
* Copyright (C) 2019 - 2021 Jonas Lochmann
|
* Copyright (C) 2019 - 2022 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
@ -39,6 +39,7 @@ const types = [
|
||||||
'LinkParentMailAddressRequest',
|
'LinkParentMailAddressRequest',
|
||||||
'UpdatePrimaryDeviceRequest',
|
'UpdatePrimaryDeviceRequest',
|
||||||
'RemoveDeviceRequest',
|
'RemoveDeviceRequest',
|
||||||
|
'RequestIdentityTokenRequest',
|
||||||
'RequestWithAuthToken',
|
'RequestWithAuthToken',
|
||||||
'SendMailLoginCodeRequest',
|
'SendMailLoginCodeRequest',
|
||||||
'SignInByMailCodeRequest'
|
'SignInByMailCodeRequest'
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* server component for the TimeLimit App
|
* server component for the TimeLimit App
|
||||||
* Copyright (C) 2019 - 2021 Jonas Lochmann
|
* Copyright (C) 2019 - 2022 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
@ -27,12 +27,13 @@ import { getStatusByMailToken } from '../function/parent/get-status-by-mail-addr
|
||||||
import { linkMailAddress } from '../function/parent/link-mail-address'
|
import { linkMailAddress } from '../function/parent/link-mail-address'
|
||||||
import { recoverParentPassword } from '../function/parent/recover-parent-password'
|
import { recoverParentPassword } from '../function/parent/recover-parent-password'
|
||||||
import { signInIntoFamily } from '../function/parent/sign-in-into-family'
|
import { signInIntoFamily } from '../function/parent/sign-in-into-family'
|
||||||
|
import { createIdentityToken, MissingSignSecretException } from '../util/identity-token'
|
||||||
import { WebsocketApi } from '../websocket'
|
import { WebsocketApi } from '../websocket'
|
||||||
import {
|
import {
|
||||||
isCreateFamilyByMailTokenRequest,
|
isCreateFamilyByMailTokenRequest,
|
||||||
isCreateRegisterDeviceTokenRequest, isLinkParentMailAddressRequest,
|
isCreateRegisterDeviceTokenRequest, isLinkParentMailAddressRequest,
|
||||||
isMailAuthTokenRequestBody, isRecoverParentPasswordRequest,
|
isMailAuthTokenRequestBody, isRecoverParentPasswordRequest,
|
||||||
isRemoveDeviceRequest, isSignIntoFamilyRequest
|
isRemoveDeviceRequest, isSignIntoFamilyRequest, isRequestIdentityTokenRequest
|
||||||
} from './validator'
|
} from './validator'
|
||||||
|
|
||||||
export const createParentRouter = ({ database, websocket }: {database: Database, websocket: WebsocketApi}) => {
|
export const createParentRouter = ({ database, websocket }: {database: Database, websocket: WebsocketApi}) => {
|
||||||
|
@ -131,7 +132,7 @@ export const createParentRouter = ({ database, websocket }: {database: Database,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
async function assertAuthValidAndReturnDeviceEntry ({ deviceAuthToken, parentId, secondPasswordHash, transaction }: {
|
async function assertAuthValidAndReturnDetails ({ deviceAuthToken, parentId, secondPasswordHash, transaction }: {
|
||||||
deviceAuthToken: string
|
deviceAuthToken: string
|
||||||
parentId: string
|
parentId: string
|
||||||
secondPasswordHash: string
|
secondPasswordHash: string
|
||||||
|
@ -165,6 +166,8 @@ export const createParentRouter = ({ database, websocket }: {database: Database,
|
||||||
if (!parentEntry) {
|
if (!parentEntry) {
|
||||||
throw new Unauthorized()
|
throw new Unauthorized()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { deviceEntry, parentEntry }
|
||||||
} else {
|
} else {
|
||||||
const parentEntry = await database.user.findOne({
|
const parentEntry = await database.user.findOne({
|
||||||
where: {
|
where: {
|
||||||
|
@ -179,9 +182,9 @@ export const createParentRouter = ({ database, websocket }: {database: Database,
|
||||||
if (!parentEntry) {
|
if (!parentEntry) {
|
||||||
throw new Unauthorized()
|
throw new Unauthorized()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return deviceEntry
|
return { deviceEntry, parentEntry }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
router.post('/create-add-device-token', json(), async (req, res, next) => {
|
router.post('/create-add-device-token', json(), async (req, res, next) => {
|
||||||
|
@ -191,7 +194,7 @@ export const createParentRouter = ({ database, websocket }: {database: Database,
|
||||||
}
|
}
|
||||||
|
|
||||||
const { token, deviceId } = await database.transaction(async (transaction) => {
|
const { token, deviceId } = await database.transaction(async (transaction) => {
|
||||||
const deviceEntry = await assertAuthValidAndReturnDeviceEntry({
|
const { deviceEntry } = await assertAuthValidAndReturnDetails({
|
||||||
deviceAuthToken: req.body.deviceAuthToken,
|
deviceAuthToken: req.body.deviceAuthToken,
|
||||||
parentId: req.body.parentId,
|
parentId: req.body.parentId,
|
||||||
secondPasswordHash: req.body.parentPasswordSecondHash,
|
secondPasswordHash: req.body.parentPasswordSecondHash,
|
||||||
|
@ -235,7 +238,7 @@ export const createParentRouter = ({ database, websocket }: {database: Database,
|
||||||
}
|
}
|
||||||
|
|
||||||
await database.transaction(async (transaction) => {
|
await database.transaction(async (transaction) => {
|
||||||
const deviceEntry = await assertAuthValidAndReturnDeviceEntry({
|
const { deviceEntry } = await assertAuthValidAndReturnDetails({
|
||||||
deviceAuthToken: req.body.deviceAuthToken,
|
deviceAuthToken: req.body.deviceAuthToken,
|
||||||
parentId: req.body.parentUserId,
|
parentId: req.body.parentUserId,
|
||||||
secondPasswordHash: req.body.parentPasswordSecondHash,
|
secondPasswordHash: req.body.parentPasswordSecondHash,
|
||||||
|
@ -257,5 +260,36 @@ export const createParentRouter = ({ database, websocket }: {database: Database,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.post('/create-identity-token', json(), async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
if (!isRequestIdentityTokenRequest(req.body)) {
|
||||||
|
throw new BadRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = req.body
|
||||||
|
|
||||||
|
await database.transaction(async (transaction) => {
|
||||||
|
const { deviceEntry, parentEntry } = await assertAuthValidAndReturnDetails({
|
||||||
|
deviceAuthToken: body.deviceAuthToken,
|
||||||
|
parentId: body.parentUserId,
|
||||||
|
secondPasswordHash: body.parentPasswordSecondHash,
|
||||||
|
transaction
|
||||||
|
})
|
||||||
|
|
||||||
|
const token = await createIdentityToken({
|
||||||
|
purpose: body.purpose,
|
||||||
|
familyId: deviceEntry.familyId,
|
||||||
|
userId: parentEntry.userId,
|
||||||
|
mail: parentEntry.mail
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json({ token })
|
||||||
|
})
|
||||||
|
} catch (ex) {
|
||||||
|
if (ex instanceof MissingSignSecretException) res.sendStatus(404)
|
||||||
|
else next(ex)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,6 +140,13 @@ export interface RemoveDeviceRequest {
|
||||||
deviceId: string
|
deviceId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RequestIdentityTokenRequest {
|
||||||
|
deviceAuthToken: string
|
||||||
|
parentUserId: string
|
||||||
|
parentPasswordSecondHash: string
|
||||||
|
purpose: 'purchase'
|
||||||
|
}
|
||||||
|
|
||||||
export interface RequestWithAuthToken {
|
export interface RequestWithAuthToken {
|
||||||
deviceAuthToken: string
|
deviceAuthToken: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// tslint:disable
|
// tslint:disable
|
||||||
import { ClientPushChangesRequest, ClientPullChangesRequest, MailAuthTokenRequestBody, CreateFamilyByMailTokenRequest, SignIntoFamilyRequest, RecoverParentPasswordRequest, RegisterChildDeviceRequest, SerializedParentAction, SerializedAppLogicAction, SerializedChildAction, CreateRegisterDeviceTokenRequest, CanDoPurchaseRequest, FinishPurchaseByGooglePlayRequest, LinkParentMailAddressRequest, UpdatePrimaryDeviceRequest, RemoveDeviceRequest, RequestWithAuthToken, SendMailLoginCodeRequest, SignInByMailCodeRequest } from './schema'
|
import { ClientPushChangesRequest, ClientPullChangesRequest, MailAuthTokenRequestBody, CreateFamilyByMailTokenRequest, SignIntoFamilyRequest, RecoverParentPasswordRequest, RegisterChildDeviceRequest, SerializedParentAction, SerializedAppLogicAction, SerializedChildAction, CreateRegisterDeviceTokenRequest, CanDoPurchaseRequest, FinishPurchaseByGooglePlayRequest, LinkParentMailAddressRequest, UpdatePrimaryDeviceRequest, RemoveDeviceRequest, RequestIdentityTokenRequest, RequestWithAuthToken, SendMailLoginCodeRequest, SignInByMailCodeRequest } from './schema'
|
||||||
import Ajv from 'ajv'
|
import Ajv from 'ajv'
|
||||||
const ajv = new Ajv()
|
const ajv = new Ajv()
|
||||||
|
|
||||||
|
@ -3253,6 +3253,35 @@ export const isRemoveDeviceRequest: (value: unknown) => value is RemoveDeviceReq
|
||||||
"definitions": definitions,
|
"definitions": definitions,
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#"
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
})
|
})
|
||||||
|
export const isRequestIdentityTokenRequest: (value: unknown) => value is RequestIdentityTokenRequest = ajv.compile({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"deviceAuthToken": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"parentUserId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"parentPasswordSecondHash": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"purpose": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"purchase"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"deviceAuthToken",
|
||||||
|
"parentPasswordSecondHash",
|
||||||
|
"parentUserId",
|
||||||
|
"purpose"
|
||||||
|
],
|
||||||
|
"definitions": definitions,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
})
|
||||||
export const isRequestWithAuthToken: (value: unknown) => value is RequestWithAuthToken = ajv.compile({
|
export const isRequestWithAuthToken: (value: unknown) => value is RequestWithAuthToken = ajv.compile({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* server component for the TimeLimit App
|
* server component for the TimeLimit App
|
||||||
* Copyright (C) 2019 - 2020 Jonas Lochmann
|
* Copyright (C) 2019 - 2022 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
@ -21,6 +21,7 @@ interface Config {
|
||||||
disableSignup: boolean
|
disableSignup: boolean
|
||||||
pingInterval: number
|
pingInterval: number
|
||||||
alwaysPro: boolean
|
alwaysPro: boolean
|
||||||
|
signSecret: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseYesNo (value: string) {
|
function parseYesNo (value: string) {
|
||||||
|
@ -37,7 +38,8 @@ export const config: Config = {
|
||||||
mailWhitelist: (process.env.MAIL_WHITELIST || '').split(',').map((item) => item.trim()).filter((item) => item.length > 0),
|
mailWhitelist: (process.env.MAIL_WHITELIST || '').split(',').map((item) => item.trim()).filter((item) => item.length > 0),
|
||||||
disableSignup: parseYesNo(process.env.DISABLE_SIGNUP || 'no'),
|
disableSignup: parseYesNo(process.env.DISABLE_SIGNUP || 'no'),
|
||||||
pingInterval: parseInt(process.env.PING_INTERVAL_SEC || '25', 10) * 1000,
|
pingInterval: parseInt(process.env.PING_INTERVAL_SEC || '25', 10) * 1000,
|
||||||
alwaysPro: process.env.ALWAYS_PRO ? parseYesNo(process.env.ALWAYS_PRO) : false
|
alwaysPro: process.env.ALWAYS_PRO ? parseYesNo(process.env.ALWAYS_PRO) : false,
|
||||||
|
signSecret: process.env.SIGN_SECRET || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
class ParseYesNoException extends Error {}
|
class ParseYesNoException extends Error {}
|
||||||
|
|
41
src/util/identity-token.ts
Normal file
41
src/util/identity-token.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* server component for the TimeLimit App
|
||||||
|
* Copyright (C) 2019 - 2022 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SignJWT } from 'jose'
|
||||||
|
import { config } from '../config'
|
||||||
|
|
||||||
|
export async function createIdentityToken({ purpose, familyId, userId, mail }: {
|
||||||
|
purpose: string
|
||||||
|
familyId: string
|
||||||
|
userId: string
|
||||||
|
mail: string
|
||||||
|
}) {
|
||||||
|
if (config.signSecret === '') throw new MissingSignSecretException()
|
||||||
|
|
||||||
|
const jwt = await new SignJWT({ purpose, familyId, userId, mail })
|
||||||
|
.setExpirationTime('7d')
|
||||||
|
.setProtectedHeader({ alg: 'HS512' })
|
||||||
|
.sign(Buffer.from(config.signSecret, 'utf8'))
|
||||||
|
|
||||||
|
return Buffer.from(jwt, 'ascii')
|
||||||
|
.toString('base64')
|
||||||
|
.split(/(.{32})/)
|
||||||
|
.filter((item) => item.length > 0)
|
||||||
|
.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MissingSignSecretException extends Error {}
|
Loading…
Add table
Add a link
Reference in a new issue