mirror of
https://codeberg.org/timelimit/timelimit-server.git
synced 2025-10-03 09:49:32 +02:00
Add new purchase API
This commit is contained in:
parent
35acd05d08
commit
58a6359a3e
18 changed files with 534 additions and 22 deletions
|
@ -89,3 +89,41 @@ If there was nothing found for the mail address: HTTP status code 409 Conflict
|
||||||
### see
|
### see
|
||||||
|
|
||||||
- [premium concept](../concept/premium.md)
|
- [premium concept](../concept/premium.md)
|
||||||
|
|
||||||
|
## POST /admin/unlock-premium-v2
|
||||||
|
|
||||||
|
Use this to unlock all features for one user for a specified duration.
|
||||||
|
|
||||||
|
### request
|
||||||
|
|
||||||
|
request properties: ``purchaseToken`` and ``purchaseId``
|
||||||
|
|
||||||
|
- ``purchasetoken`` is a string which the client shows at the purchase screen
|
||||||
|
- ``purchaseId`` is the ID that is used at the bill
|
||||||
|
|
||||||
|
### response
|
||||||
|
|
||||||
|
The response contains the following properties:
|
||||||
|
|
||||||
|
- ``ok`` (boolean)
|
||||||
|
- ``error``
|
||||||
|
- string
|
||||||
|
- set if and only if ``ok`` is false
|
||||||
|
- possible values
|
||||||
|
- ``token invalid``
|
||||||
|
- ``illegal state``
|
||||||
|
- ``purchase id already used``
|
||||||
|
- ``detail``
|
||||||
|
- optional string
|
||||||
|
- should be shown to the support
|
||||||
|
- ``lastPurchase``
|
||||||
|
- optional object
|
||||||
|
- should be shown to the support
|
||||||
|
- ``wasAlreadyExecuted`` (boolean, set if and only if ``ok`` is true)
|
||||||
|
|
||||||
|
If the request was malformed: HTTP status code 400 Bad Request
|
||||||
|
|
||||||
|
Using the same ``purchaseId`` twice results in:
|
||||||
|
|
||||||
|
- ``wasAlreadyExecuted`` if the familyId is unchanged
|
||||||
|
- ``error`` = ``purchase id already used`` otherwise
|
||||||
|
|
35
docs/schema/IdentityTokenPayload.schema.json
Normal file
35
docs/schema/IdentityTokenPayload.schema.json
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"additionalProperties": false,
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"purpose": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"purchase"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"familyId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"mail": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"exp": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"exp",
|
||||||
|
"familyId",
|
||||||
|
"mail",
|
||||||
|
"purpose",
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"definitions": {},
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "IdentityTokenPayload",
|
||||||
|
"$id": "https://timelimit.io/IdentityTokenPayload"
|
||||||
|
}
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
* [FinishPurchaseByGooglePlayRequest](./finishpurchasebygoogleplayrequest.md) – `https://timelimit.io/FinishPurchaseByGooglePlayRequest`
|
* [FinishPurchaseByGooglePlayRequest](./finishpurchasebygoogleplayrequest.md) – `https://timelimit.io/FinishPurchaseByGooglePlayRequest`
|
||||||
|
|
||||||
|
* [IdentityTokenPayload](./identitytokenpayload.md) – `https://timelimit.io/IdentityTokenPayload`
|
||||||
|
|
||||||
* [LinkParentMailAddressRequest](./linkparentmailaddressrequest.md) – `https://timelimit.io/LinkParentMailAddressRequest`
|
* [LinkParentMailAddressRequest](./linkparentmailaddressrequest.md) – `https://timelimit.io/LinkParentMailAddressRequest`
|
||||||
|
|
||||||
* [MailAuthTokenRequestBody](./mailauthtokenrequestbody.md) – `https://timelimit.io/MailAuthTokenRequestBody`
|
* [MailAuthTokenRequestBody](./mailauthtokenrequestbody.md) – `https://timelimit.io/MailAuthTokenRequestBody`
|
||||||
|
|
15
docs/schema/identitytokenpayload-definitions.md
Normal file
15
docs/schema/identitytokenpayload-definitions.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# Untitled undefined type in IdentityTokenPayload Schema
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://timelimit.io/IdentityTokenPayload#/definitions
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
|
||||||
|
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------- |
|
||||||
|
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [IdentityTokenPayload.schema.json\*](IdentityTokenPayload.schema.json "open original schema") |
|
||||||
|
|
||||||
|
## definitions Type
|
||||||
|
|
||||||
|
unknown
|
15
docs/schema/identitytokenpayload-properties-exp.md
Normal file
15
docs/schema/identitytokenpayload-properties-exp.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# Untitled number in IdentityTokenPayload Schema
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://timelimit.io/IdentityTokenPayload#/properties/exp
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
|
||||||
|
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------- |
|
||||||
|
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [IdentityTokenPayload.schema.json\*](IdentityTokenPayload.schema.json "open original schema") |
|
||||||
|
|
||||||
|
## exp Type
|
||||||
|
|
||||||
|
`number`
|
15
docs/schema/identitytokenpayload-properties-familyid.md
Normal file
15
docs/schema/identitytokenpayload-properties-familyid.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# Untitled string in IdentityTokenPayload Schema
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://timelimit.io/IdentityTokenPayload#/properties/familyId
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
|
||||||
|
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------- |
|
||||||
|
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [IdentityTokenPayload.schema.json\*](IdentityTokenPayload.schema.json "open original schema") |
|
||||||
|
|
||||||
|
## familyId Type
|
||||||
|
|
||||||
|
`string`
|
15
docs/schema/identitytokenpayload-properties-mail.md
Normal file
15
docs/schema/identitytokenpayload-properties-mail.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# Untitled string in IdentityTokenPayload Schema
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://timelimit.io/IdentityTokenPayload#/properties/mail
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
|
||||||
|
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------- |
|
||||||
|
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [IdentityTokenPayload.schema.json\*](IdentityTokenPayload.schema.json "open original schema") |
|
||||||
|
|
||||||
|
## mail Type
|
||||||
|
|
||||||
|
`string`
|
23
docs/schema/identitytokenpayload-properties-purpose.md
Normal file
23
docs/schema/identitytokenpayload-properties-purpose.md
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Untitled string in IdentityTokenPayload Schema
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://timelimit.io/IdentityTokenPayload#/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 | [IdentityTokenPayload.schema.json\*](IdentityTokenPayload.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"` | |
|
15
docs/schema/identitytokenpayload-properties-userid.md
Normal file
15
docs/schema/identitytokenpayload-properties-userid.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# Untitled string in IdentityTokenPayload Schema
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://timelimit.io/IdentityTokenPayload#/properties/userId
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
|
||||||
|
| :------------------ | :--------- | :------------- | :---------------------- | :---------------- | :-------------------- | :------------------ | :-------------------------------------------------------------------------------------------- |
|
||||||
|
| Can be instantiated | No | Unknown status | Unknown identifiability | Forbidden | Allowed | none | [IdentityTokenPayload.schema.json\*](IdentityTokenPayload.schema.json "open original schema") |
|
||||||
|
|
||||||
|
## userId Type
|
||||||
|
|
||||||
|
`string`
|
125
docs/schema/identitytokenpayload.md
Normal file
125
docs/schema/identitytokenpayload.md
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
# IdentityTokenPayload Schema
|
||||||
|
|
||||||
|
```txt
|
||||||
|
https://timelimit.io/IdentityTokenPayload
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
| Abstract | Extensible | Status | Identifiable | Custom Properties | Additional Properties | Access Restrictions | Defined In |
|
||||||
|
| :------------------ | :--------- | :------------- | :----------- | :---------------- | :-------------------- | :------------------ | :------------------------------------------------------------------------------------------ |
|
||||||
|
| Can be instantiated | Yes | Unknown status | No | Forbidden | Forbidden | none | [IdentityTokenPayload.schema.json](IdentityTokenPayload.schema.json "open original schema") |
|
||||||
|
|
||||||
|
## IdentityTokenPayload Type
|
||||||
|
|
||||||
|
`object` ([IdentityTokenPayload](identitytokenpayload.md))
|
||||||
|
|
||||||
|
# IdentityTokenPayload Properties
|
||||||
|
|
||||||
|
| Property | Type | Required | Nullable | Defined by |
|
||||||
|
| :-------------------- | :------- | :------- | :------------- | :----------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| [purpose](#purpose) | `string` | Required | cannot be null | [IdentityTokenPayload](identitytokenpayload-properties-purpose.md "https://timelimit.io/IdentityTokenPayload#/properties/purpose") |
|
||||||
|
| [familyId](#familyid) | `string` | Required | cannot be null | [IdentityTokenPayload](identitytokenpayload-properties-familyid.md "https://timelimit.io/IdentityTokenPayload#/properties/familyId") |
|
||||||
|
| [userId](#userid) | `string` | Required | cannot be null | [IdentityTokenPayload](identitytokenpayload-properties-userid.md "https://timelimit.io/IdentityTokenPayload#/properties/userId") |
|
||||||
|
| [mail](#mail) | `string` | Required | cannot be null | [IdentityTokenPayload](identitytokenpayload-properties-mail.md "https://timelimit.io/IdentityTokenPayload#/properties/mail") |
|
||||||
|
| [exp](#exp) | `number` | Required | cannot be null | [IdentityTokenPayload](identitytokenpayload-properties-exp.md "https://timelimit.io/IdentityTokenPayload#/properties/exp") |
|
||||||
|
|
||||||
|
## purpose
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`purpose`
|
||||||
|
|
||||||
|
* is required
|
||||||
|
|
||||||
|
* Type: `string`
|
||||||
|
|
||||||
|
* cannot be null
|
||||||
|
|
||||||
|
* defined in: [IdentityTokenPayload](identitytokenpayload-properties-purpose.md "https://timelimit.io/IdentityTokenPayload#/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"` | |
|
||||||
|
|
||||||
|
## familyId
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`familyId`
|
||||||
|
|
||||||
|
* is required
|
||||||
|
|
||||||
|
* Type: `string`
|
||||||
|
|
||||||
|
* cannot be null
|
||||||
|
|
||||||
|
* defined in: [IdentityTokenPayload](identitytokenpayload-properties-familyid.md "https://timelimit.io/IdentityTokenPayload#/properties/familyId")
|
||||||
|
|
||||||
|
### familyId Type
|
||||||
|
|
||||||
|
`string`
|
||||||
|
|
||||||
|
## userId
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`userId`
|
||||||
|
|
||||||
|
* is required
|
||||||
|
|
||||||
|
* Type: `string`
|
||||||
|
|
||||||
|
* cannot be null
|
||||||
|
|
||||||
|
* defined in: [IdentityTokenPayload](identitytokenpayload-properties-userid.md "https://timelimit.io/IdentityTokenPayload#/properties/userId")
|
||||||
|
|
||||||
|
### userId Type
|
||||||
|
|
||||||
|
`string`
|
||||||
|
|
||||||
|
## mail
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`mail`
|
||||||
|
|
||||||
|
* is required
|
||||||
|
|
||||||
|
* Type: `string`
|
||||||
|
|
||||||
|
* cannot be null
|
||||||
|
|
||||||
|
* defined in: [IdentityTokenPayload](identitytokenpayload-properties-mail.md "https://timelimit.io/IdentityTokenPayload#/properties/mail")
|
||||||
|
|
||||||
|
### mail Type
|
||||||
|
|
||||||
|
`string`
|
||||||
|
|
||||||
|
## exp
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
`exp`
|
||||||
|
|
||||||
|
* is required
|
||||||
|
|
||||||
|
* Type: `number`
|
||||||
|
|
||||||
|
* cannot be null
|
||||||
|
|
||||||
|
* defined in: [IdentityTokenPayload](identitytokenpayload-properties-exp.md "https://timelimit.io/IdentityTokenPayload#/properties/exp")
|
||||||
|
|
||||||
|
### exp Type
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
# IdentityTokenPayload Definitions
|
|
@ -42,7 +42,8 @@ const types = [
|
||||||
'RequestIdentityTokenRequest',
|
'RequestIdentityTokenRequest',
|
||||||
'RequestWithAuthToken',
|
'RequestWithAuthToken',
|
||||||
'SendMailLoginCodeRequest',
|
'SendMailLoginCodeRequest',
|
||||||
'SignInByMailCodeRequest'
|
'SignInByMailCodeRequest',
|
||||||
|
'IdentityTokenPayload'
|
||||||
]
|
]
|
||||||
|
|
||||||
const docOnlyTypes = [
|
const docOnlyTypes = [
|
||||||
|
|
153
src/api/admin.ts
153
src/api/admin.ts
|
@ -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
|
||||||
|
@ -18,11 +18,13 @@
|
||||||
import { json } from 'body-parser'
|
import { json } from 'body-parser'
|
||||||
import { Router } from 'express'
|
import { Router } from 'express'
|
||||||
import { BadRequest, Conflict } from 'http-errors'
|
import { BadRequest, Conflict } from 'http-errors'
|
||||||
|
import * as Sequelize from 'sequelize'
|
||||||
import { Database } from '../database'
|
import { Database } from '../database'
|
||||||
import { addPurchase } from '../function/purchase'
|
import { addPurchase, canDoNextPurchase } from '../function/purchase'
|
||||||
import { getStatusMessage, setStatusMessage } from '../function/statusmessage'
|
import { getStatusMessage, setStatusMessage } from '../function/statusmessage'
|
||||||
import { EventHandler } from '../monitoring/eventhandler'
|
import { EventHandler } from '../monitoring/eventhandler'
|
||||||
import { generatePurchaseId } from '../util/token'
|
import { generatePurchaseId } from '../util/token'
|
||||||
|
import { verifyIdentitifyToken, TokenValidationException } from '../util/identity-token'
|
||||||
import { WebsocketApi } from '../websocket'
|
import { WebsocketApi } from '../websocket'
|
||||||
|
|
||||||
export const createAdminRouter = ({ database, websocket, eventHandler }: {
|
export const createAdminRouter = ({ database, websocket, eventHandler }: {
|
||||||
|
@ -120,7 +122,8 @@ export const createAdminRouter = ({ database, websocket, eventHandler }: {
|
||||||
database,
|
database,
|
||||||
familyId: userEntry.familyId,
|
familyId: userEntry.familyId,
|
||||||
type,
|
type,
|
||||||
transactionId: 'manual-' + type + '-' + generatePurchaseId(),
|
service: 'directpurchase',
|
||||||
|
transactionId: 'legacyunlock-' + type + '-' + generatePurchaseId(),
|
||||||
websocket,
|
websocket,
|
||||||
transaction
|
transaction
|
||||||
})
|
})
|
||||||
|
@ -132,5 +135,149 @@ export const createAdminRouter = ({ database, websocket, eventHandler }: {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.post('/unlock-premium-v2', json(), async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
typeof req.body !== 'object' ||
|
||||||
|
typeof req.body.purchaseToken !== 'string' ||
|
||||||
|
typeof req.body.purchaseId !== 'string'
|
||||||
|
) {
|
||||||
|
throw new BadRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
const purchaseToken: string = req.body.purchaseToken
|
||||||
|
const purchaseId: string = req.body.purchaseId
|
||||||
|
|
||||||
|
const tokenContent = await verifyIdentitifyToken(purchaseToken)
|
||||||
|
|
||||||
|
if (tokenContent.purpose !== 'purchase') {
|
||||||
|
res.json({ ok: false, error: 'token invalid', detail: 'wrong purpose' })
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await database.transaction(async (transaction) => {
|
||||||
|
const userValid = await database.user.count({
|
||||||
|
where: {
|
||||||
|
familyId: tokenContent.familyId,
|
||||||
|
userId: tokenContent.userId,
|
||||||
|
mail: tokenContent.mail,
|
||||||
|
type: 'parent'
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!userValid) return {
|
||||||
|
ok: false,
|
||||||
|
error: 'token invalid',
|
||||||
|
detail: 'user not found'
|
||||||
|
}
|
||||||
|
|
||||||
|
let mailToReturn: string
|
||||||
|
|
||||||
|
if (tokenContent.mail !== '') mailToReturn = tokenContent.mail
|
||||||
|
else {
|
||||||
|
const userEntryWithMail = await database.user.findOne({
|
||||||
|
where: {
|
||||||
|
familyId: tokenContent.familyId,
|
||||||
|
mail: {
|
||||||
|
[Sequelize.Op.ne]: ''
|
||||||
|
},
|
||||||
|
type: 'parent'
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!userEntryWithMail) return {
|
||||||
|
ok: false,
|
||||||
|
error: 'illegal state',
|
||||||
|
detail: 'no user with mail found'
|
||||||
|
}
|
||||||
|
|
||||||
|
mailToReturn = userEntryWithMail.mail
|
||||||
|
}
|
||||||
|
|
||||||
|
let wasAlreadyExecuted: boolean
|
||||||
|
|
||||||
|
const oldPurchaseByPurchaseId = await database.purchase.findOne({
|
||||||
|
where: {
|
||||||
|
service: 'directpurchase',
|
||||||
|
transactionId: purchaseId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (oldPurchaseByPurchaseId === null) wasAlreadyExecuted = false
|
||||||
|
else if (oldPurchaseByPurchaseId.familyId === tokenContent.familyId) wasAlreadyExecuted = true
|
||||||
|
else return {
|
||||||
|
ok: false,
|
||||||
|
error: 'purchase id already used'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wasAlreadyExecuted) {
|
||||||
|
const familyEntry = await database.family.findOne({
|
||||||
|
where: {
|
||||||
|
familyId: tokenContent.familyId
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!familyEntry) return {
|
||||||
|
ok: false,
|
||||||
|
error: 'family not found'
|
||||||
|
}
|
||||||
|
|
||||||
|
const canDoPurchase = canDoNextPurchase({ fullVersionUntil: parseInt(familyEntry.fullVersionUntil) })
|
||||||
|
|
||||||
|
if (!canDoPurchase) {
|
||||||
|
const lastPurchase = await database.purchase.findOne({
|
||||||
|
where: {
|
||||||
|
familyId: tokenContent.familyId
|
||||||
|
},
|
||||||
|
transaction,
|
||||||
|
order: [['loggedAt', 'DESC']],
|
||||||
|
limit: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: 'can not renew now',
|
||||||
|
lastPurchase: lastPurchase ? {
|
||||||
|
service: lastPurchase.service,
|
||||||
|
transactionId: lastPurchase.transactionId,
|
||||||
|
timestamp: parseInt(lastPurchase.loggedAt),
|
||||||
|
timestring: new Date(parseInt(lastPurchase.loggedAt)).toISOString()
|
||||||
|
} : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await addPurchase({
|
||||||
|
database,
|
||||||
|
familyId: tokenContent.familyId,
|
||||||
|
type: 'year',
|
||||||
|
service: 'directpurchase',
|
||||||
|
transactionId: purchaseId,
|
||||||
|
websocket,
|
||||||
|
transaction
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
mail: mailToReturn,
|
||||||
|
wasAlreadyExecuted
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json(response)
|
||||||
|
} catch (ex) {
|
||||||
|
if (ex instanceof TokenValidationException) res.json({
|
||||||
|
ok: false,
|
||||||
|
error: 'token invalid',
|
||||||
|
detail: ex.message
|
||||||
|
})
|
||||||
|
else next(ex)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
@ -122,6 +122,7 @@ export const createPurchaseRouter = ({ database, websocket }: {
|
||||||
database,
|
database,
|
||||||
familyId: deviceEntry.familyId,
|
familyId: deviceEntry.familyId,
|
||||||
type,
|
type,
|
||||||
|
service: 'googleplay',
|
||||||
transactionId: orderId,
|
transactionId: orderId,
|
||||||
websocket,
|
websocket,
|
||||||
transaction
|
transaction
|
||||||
|
|
|
@ -162,5 +162,16 @@ export interface SignInByMailCodeRequest {
|
||||||
receivedCode: string
|
receivedCode: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IdentityTokenCreatePayload {
|
||||||
|
purpose: 'purchase'
|
||||||
|
familyId: string
|
||||||
|
userId: string
|
||||||
|
mail: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IdentityTokenPayload = IdentityTokenCreatePayload & {
|
||||||
|
exp: number
|
||||||
|
}
|
||||||
|
|
||||||
export { SerializedParentAction, SerializedChildAction, SerializedAppLogicAction } from '../action/serialization'
|
export { SerializedParentAction, SerializedChildAction, SerializedAppLogicAction } from '../action/serialization'
|
||||||
export { ServerDataStatus } from '../object/serverdatastatus'
|
export { ServerDataStatus } from '../object/serverdatastatus'
|
||||||
|
|
|
@ -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, RequestIdentityTokenRequest, 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, IdentityTokenPayload } from './schema'
|
||||||
import Ajv from 'ajv'
|
import Ajv from 'ajv'
|
||||||
const ajv = new Ajv()
|
const ajv = new Ajv()
|
||||||
|
|
||||||
|
@ -3451,3 +3451,36 @@ export const isSignInByMailCodeRequest: (value: unknown) => value is SignInByMai
|
||||||
"definitions": definitions,
|
"definitions": definitions,
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#"
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
})
|
})
|
||||||
|
export const isIdentityTokenPayload: (value: unknown) => value is IdentityTokenPayload = ajv.compile({
|
||||||
|
"additionalProperties": false,
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"purpose": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"purchase"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"familyId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"mail": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"exp": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"exp",
|
||||||
|
"familyId",
|
||||||
|
"mail",
|
||||||
|
"purpose",
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"definitions": definitions,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
})
|
||||||
|
|
|
@ -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,7 +21,7 @@ import { SequelizeAttributes } from './types'
|
||||||
|
|
||||||
export interface PurchaseAttributes {
|
export interface PurchaseAttributes {
|
||||||
familyId: string
|
familyId: string
|
||||||
service: 'googleplay'
|
service: 'googleplay' | 'directpurchase'
|
||||||
transactionId: string
|
transactionId: string
|
||||||
type: 'month' | 'year'
|
type: 'month' | 'year'
|
||||||
loggedAt: string
|
loggedAt: string
|
||||||
|
@ -37,7 +37,7 @@ export type PurchaseModelStatic = typeof Sequelize.Model & {
|
||||||
export const attributes: SequelizeAttributes<PurchaseAttributes> = {
|
export const attributes: SequelizeAttributes<PurchaseAttributes> = {
|
||||||
familyId: { ...familyIdColumn },
|
familyId: { ...familyIdColumn },
|
||||||
service: {
|
service: {
|
||||||
...createEnumColumn(['googleplay']),
|
...createEnumColumn(['googleplay', 'directpurchase']),
|
||||||
primaryKey: true
|
primaryKey: true
|
||||||
},
|
},
|
||||||
transactionId: {
|
transactionId: {
|
||||||
|
|
|
@ -24,16 +24,15 @@ const day = 1000 * 60 * 60 * 24
|
||||||
const month = day * 31
|
const month = day * 31
|
||||||
const year = day * 366
|
const year = day * 366
|
||||||
|
|
||||||
export const addPurchase = async ({ database, familyId, type, transactionId, websocket, transaction }: {
|
export const addPurchase = async ({ database, familyId, type, service, transactionId, websocket, transaction }: {
|
||||||
database: Database
|
database: Database
|
||||||
familyId: string
|
familyId: string
|
||||||
type: 'month' | 'year'
|
type: 'month' | 'year'
|
||||||
|
service: 'googleplay' | 'directpurchase'
|
||||||
transactionId: string
|
transactionId: string
|
||||||
websocket: WebsocketApi
|
websocket: WebsocketApi
|
||||||
transaction: Transaction
|
transaction: Transaction
|
||||||
}) => {
|
}) => {
|
||||||
const service = 'googleplay'
|
|
||||||
|
|
||||||
const oldPurchaseEntry = await database.purchase.findOne({
|
const oldPurchaseEntry = await database.purchase.findOne({
|
||||||
where: {
|
where: {
|
||||||
service,
|
service,
|
||||||
|
|
|
@ -15,21 +15,16 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SignJWT } from 'jose'
|
import { SignJWT, jwtVerify } from 'jose'
|
||||||
import { config } from '../config'
|
import { config } from '../config'
|
||||||
|
import { IdentityTokenPayload, IdentityTokenCreatePayload } from '../api/schema'
|
||||||
|
import { isIdentityTokenPayload } from '../api/validator'
|
||||||
|
|
||||||
export async function createIdentityToken({ purpose, familyId, userId, mail }: {
|
export async function createIdentityToken({ purpose, familyId, userId, mail }: IdentityTokenCreatePayload) {
|
||||||
purpose: string
|
|
||||||
familyId: string
|
|
||||||
userId: string
|
|
||||||
mail: string
|
|
||||||
}) {
|
|
||||||
if (config.signSecret === '') throw new MissingSignSecretException()
|
|
||||||
|
|
||||||
const jwt = await new SignJWT({ purpose, familyId, userId, mail })
|
const jwt = await new SignJWT({ purpose, familyId, userId, mail })
|
||||||
.setExpirationTime('7d')
|
.setExpirationTime('7d')
|
||||||
.setProtectedHeader({ alg: 'HS512' })
|
.setProtectedHeader({ alg: 'HS512' })
|
||||||
.sign(Buffer.from(config.signSecret, 'utf8'))
|
.sign(getSignSecret())
|
||||||
|
|
||||||
return Buffer.from(jwt, 'ascii')
|
return Buffer.from(jwt, 'ascii')
|
||||||
.toString('base64')
|
.toString('base64')
|
||||||
|
@ -38,4 +33,31 @@ export async function createIdentityToken({ purpose, familyId, userId, mail }: {
|
||||||
.join('\n')
|
.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function verifyIdentitifyToken(token: string): Promise<IdentityTokenPayload> {
|
||||||
|
try {
|
||||||
|
const { payload } = await jwtVerify(
|
||||||
|
Buffer.from(token, 'base64').toString('ascii'),
|
||||||
|
getSignSecret(),
|
||||||
|
{ algorithms: ['HS512'] }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isIdentityTokenPayload(payload)) throw new BadPayloadException()
|
||||||
|
|
||||||
|
return payload
|
||||||
|
} catch (ex) {
|
||||||
|
if (ex instanceof TokenValidationException) throw ex
|
||||||
|
else if (ex instanceof Error) throw new TokenValidationException(ex.message)
|
||||||
|
else throw ex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSignSecret(): Buffer {
|
||||||
|
if (config.signSecret === '') throw new MissingSignSecretException()
|
||||||
|
|
||||||
|
return Buffer.from(config.signSecret, 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
export class MissingSignSecretException extends Error {}
|
export class MissingSignSecretException extends Error {}
|
||||||
|
|
||||||
|
export class TokenValidationException extends Error {}
|
||||||
|
class BadPayloadException extends TokenValidationException { constructor() { super('bad payload') } }
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue