Compare commits

...
21 Commits
Author SHA1 Message Date
Jonas Lochmann 7df7790288 Update nodemailer 2026-08-03 02:00:00 +02:00
Jonas Lochmann 1aee08f409 Update dependencies 2026-08-03 02:00:00 +02:00
Jonas Lochmann 85cd9f3bb0 update dependencies 2026-07-20 02:00:00 +02:00
Jonas Lochmann 52bde0bc23 only check for google play purchase support of this method is requested 2026-07-13 02:00:00 +02:00
Jonas Lochmann 1e5e74659e undo incorrect purchase api documentation changes 2026-07-13 02:00:00 +02:00
Jonas Lochmann 893f3664cf add update notification campaign 2026-07-13 02:00:00 +02:00
Jonas Lochmann ccfbf97d36 adjust isAtLeastAsStrictAsPreviously calculation 2026-06-15 02:00:00 +02:00
Jonas Lochmann bf338f4aff fix documentation of unlock-premium-v2 2026-06-15 02:00:00 +02:00
Jonas Lochmann 91cb44d244 add expiry tolerance if not dryRun to the admin premium unlock api 2026-06-15 02:00:00 +02:00
Jonas Lochmann 235115168f add unpaid14 to the premium unlock api 2026-06-15 02:00:00 +02:00
Jonas Lochmann b2bcc3715f Update dependencies 2026-06-15 02:00:00 +02:00
Jonas Lochmann 2f35b969bf make CanDoPurchaseRequest.type optional 2026-06-08 02:00:00 +02:00
Jonas Lochmann 48d6a061f8 add full version debts system
foundation for granting the full version while consuming the same amount
automatically at the next purchase
2026-06-08 02:00:00 +02:00
Jonas Lochmann 0b6ece0d28 Update dependencies 2026-05-18 02:00:00 +02:00
Jonas Lochmann 271b27ffe9 Update dependencies 2026-05-18 02:00:00 +02:00
jonas-l b80b74b072 Merge pull request 'Fix eslint COPY in Dockerfile' (#46) from jaakristioja/timelimit-server:fix-eslint-in-dockerfile into master
Reviewed-on: https://codeberg.org/timelimit/timelimit-server/pulls/46
2026-05-17 15:16:39 +02:00
Jaak Ristioja 190ae54602 Fix eslint COPY in Dockerfile
Broken since commit a13656b27d.
2026-05-14 23:42:11 +03:00
Jonas Lochmann eb87d331b3 Update dependencies 2026-05-04 02:00:00 +02:00
Jonas Lochmann 5a6f37e4c7 Update dependencies 2026-04-06 02:00:00 +02:00
Jonas Lochmann bd4b5b206b Update dependencies 2026-03-30 02:00:00 +02:00
Jonas Lochmann a8db1ec1a9 Update dependencies 2026-03-30 02:00:00 +02:00
22 changed files with 528 additions and 557 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
# Install app dependencies
COPY package.json package-lock.json tsconfig.json .eslintignore .eslintrc.js Readme.md /usr/src/app/
COPY package.json package-lock.json tsconfig.json eslint.config.mjs Readme.md /usr/src/app/
COPY src/ /usr/src/app/src/
COPY scripts/ /usr/src/app/scripts/
COPY other/ /usr/src/app/other/
+5 -1
View File
@@ -100,7 +100,10 @@ request properties: ``purchaseToken``, ``purchaseId`` and ``dryRun``
- ``purchasetoken`` is a string which the client shows at the purchase screen
- ``purchaseId`` is the ID that is used at the bill
- ``dryRun`` is a boolean; setting true will skip the actual unlocking
- ``dryRun`` is a boolean
- setting true will skip the actual unlocking
- false will add a four week tolerance to the token expiry to permit checking now and unlocking later
- ``type`` is a string and must be ``year``, ``month`` or ``unpaid14``
### response
@@ -121,6 +124,7 @@ The response contains the following properties:
- optional object
- should be shown to the support
- ``wasAlreadyExecuted`` (boolean, set if and only if ``ok`` is true)
- ``mail`` (string, set if and only if ``ok`` is true)
If the request was malformed: HTTP status code 400 Bad Request
+2
View File
@@ -15,6 +15,8 @@ Use this before a purchase to check if a purchase is possible.
see [this JSON schema](../schema/candopurchaserequest.md)
Note that ``type`` is not used at all.
### response
On a invalid request body: HTTP status code 400 Bad request
+1 -2
View File
@@ -14,8 +14,7 @@
},
"additionalProperties": false,
"required": [
"deviceAuthToken",
"type"
"deviceAuthToken"
],
"definitions": {},
"$schema": "http://json-schema.org/draft-07/schema#",
+2 -2
View File
@@ -18,7 +18,7 @@ https://timelimit.io/CanDoPurchaseRequest
| Property | Type | Required | Nullable | Defined by |
| :---------------------------------- | :------- | :------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- |
| [type](#type) | `string` | Required | cannot be null | [CanDoPurchaseRequest](candopurchaserequest-properties-type.md "https://timelimit.io/CanDoPurchaseRequest#/properties/type") |
| [type](#type) | `string` | Optional | cannot be null | [CanDoPurchaseRequest](candopurchaserequest-properties-type.md "https://timelimit.io/CanDoPurchaseRequest#/properties/type") |
| [deviceAuthToken](#deviceauthtoken) | `string` | Required | cannot be null | [CanDoPurchaseRequest](candopurchaserequest-properties-deviceauthtoken.md "https://timelimit.io/CanDoPurchaseRequest#/properties/deviceAuthToken") |
## type
@@ -27,7 +27,7 @@ https://timelimit.io/CanDoPurchaseRequest
`type`
* is required
* is optional
* Type: `string`
+294 -510
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -55,7 +55,7 @@
"jose": "^4.9.3",
"lodash": "^4.17.21",
"mariadb": "^2.5.2",
"nodemailer": "^8.0.4",
"nodemailer": "^9.0.4",
"pg": "^8.5.1",
"pg-hstore": "^2.3.3",
"rate-limiter-flexible": "^2.1.15",
+14 -5
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
* Copyright (C) 2019 - 2026 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
@@ -141,7 +141,8 @@ export const createAdminRouter = ({ database, websocket, eventHandler }: {
typeof req.body !== 'object' ||
typeof req.body.purchaseToken !== 'string' ||
typeof req.body.purchaseId !== 'string' ||
typeof req.body.dryRun !== 'boolean'
typeof req.body.dryRun !== 'boolean' ||
typeof req.body.type !== 'string'
) {
throw new BadRequest()
}
@@ -149,8 +150,13 @@ export const createAdminRouter = ({ database, websocket, eventHandler }: {
const purchaseToken: string = req.body.purchaseToken
const purchaseId: string = req.body.purchaseId
const dryRun: boolean = req.body.dryRun
const type: string = req.body.type
const tokenContent = await verifyIdentitifyToken(purchaseToken)
if (type !== 'month' && type !== 'year' && type !== 'unpaid14') {
throw new BadRequest()
}
const tokenContent = await verifyIdentitifyToken(purchaseToken, dryRun)
if (tokenContent.purpose !== 'purchase') {
res.json({ ok: false, error: 'token invalid', detail: 'wrong purpose' })
@@ -228,7 +234,10 @@ export const createAdminRouter = ({ database, websocket, eventHandler }: {
error: 'family not found'
}
const canDoPurchase = canDoNextPurchase({ fullVersionUntil: parseInt(familyEntry.fullVersionUntil) })
const canDoPurchase = canDoNextPurchase({
fullVersionUntil: parseInt(familyEntry.fullVersionUntil),
fullVersionDebts: parseInt(familyEntry.fullVersionDebts),
})
if (!canDoPurchase) {
const lastPurchase = await database.purchase.findOne({
@@ -256,7 +265,7 @@ export const createAdminRouter = ({ database, websocket, eventHandler }: {
await addPurchase({
database,
familyId: tokenContent.familyId,
type: 'year',
type,
service: 'directpurchase',
transactionId: purchaseId,
websocket,
+10 -7
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
* Copyright (C) 2019 - 2026 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
@@ -37,16 +37,16 @@ export const createPurchaseRouter = ({ database, websocket }: {
const router = Router()
router.post('/can-do-purchase', json(), async (req, res, next) => {
if (!areGooglePlayPaymentsPossible) {
res.json({ canDoPurchase: 'no because not supported by the server' })
return
}
try {
if (!isCanDoPurchaseRequest(req.body)) {
throw new BadRequest()
}
if (req.body.type === 'googleplay' && !areGooglePlayPaymentsPossible) {
res.json({ canDoPurchase: 'no because not supported by the server' })
return
}
const result: boolean = await database.transaction(async (transaction) => {
const familyEntry = await requireFamilyEntry({
database,
@@ -54,7 +54,10 @@ export const createPurchaseRouter = ({ database, websocket }: {
transaction
})
return canDoNextPurchase({ fullVersionUntil: parseInt(familyEntry.fullVersionUntil, 10) })
return canDoNextPurchase({
fullVersionUntil: parseInt(familyEntry.fullVersionUntil, 10),
fullVersionDebts: parseInt(familyEntry.fullVersionDebts, 10),
})
})
res.json({
+1 -1
View File
@@ -113,7 +113,7 @@ export interface CreateRegisterDeviceTokenRequest {
}
export interface CanDoPurchaseRequest {
type: 'googleplay' | 'any'
type?: 'googleplay' | 'any'
deviceAuthToken: string
}
+1 -2
View File
@@ -3353,8 +3353,7 @@ export const isCanDoPurchaseRequest: (value: unknown) => value is CanDoPurchaseR
},
"additionalProperties": false,
"required": [
"deviceAuthToken",
"type"
"deviceAuthToken"
],
"definitions": definitions,
"$schema": "http://json-schema.org/draft-07/schema#"
+20 -3
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
* Copyright (C) 2019 - 2026 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
@@ -37,8 +37,13 @@ export interface FamilyAttributesVersion3 {
u2fKeysVersion: string
}
export interface FamilyAttributesVersion4 {
fullVersionDebts: string
}
export type FamilyAttributes = FamilyAttributesVersion1 &
FamilyAttributesVersion2 & FamilyAttributesVersion3
FamilyAttributesVersion2 & FamilyAttributesVersion3 &
FamilyAttributesVersion4
export type FamilyModel = Sequelize.Model<FamilyAttributes> & FamilyAttributes
export type FamilyModelStatic = typeof Sequelize.Model & {
@@ -76,10 +81,22 @@ export const attributesVersion3: SequelizeAttributes<FamilyAttributesVersion3> =
}
}
export const attributesVersion4: SequelizeAttributes<FamilyAttributesVersion4> = {
fullVersionDebts: {
type: Sequelize.BIGINT,
allowNull: false,
defaultValue: 0,
validate: {
min: 0
}
}
}
export const attributes: SequelizeAttributes<FamilyAttributes> = {
...attributesVersion1,
...attributesVersion2,
...attributesVersion3
...attributesVersion3,
...attributesVersion4
}
export const createFamilyModel = (sequelize: Sequelize.Sequelize): FamilyModelStatic => sequelize.define('Family', attributes) as FamilyModelStatic
@@ -0,0 +1,39 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2026 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 { QueryInterface, Sequelize, Transaction } from 'sequelize'
import { attributesVersion4 as familyAttributes } from '../../family'
export async function up (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
await queryInterface.addColumn('Families', 'fullVersionDebts', {
...familyAttributes.fullVersionDebts
}, {
transaction
})
})
}
export async function down (queryInterface: QueryInterface, sequelize: Sequelize) {
await sequelize.transaction({
type: Transaction.TYPES.EXCLUSIVE
}, async (transaction) => {
await queryInterface.removeColumn('Families', 'fullVersionDebts', { transaction })
})
}
+3 -3
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
* Copyright (C) 2019 - 2026 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
@@ -23,7 +23,7 @@ export interface PurchaseAttributes {
familyId: string
service: 'googleplay' | 'directpurchase'
transactionId: string
type: 'month' | 'year'
type: 'month' | 'year' | 'unpaid14'
loggedAt: string
previousFullVersionEndTime: string
newFullVersionEndTime: string
@@ -44,7 +44,7 @@ export const attributes: SequelizeAttributes<PurchaseAttributes> = {
type: Sequelize.STRING,
primaryKey: true
},
type: createEnumColumn(['month', 'year']),
type: createEnumColumn(['month', 'year', 'unpaid14']),
loggedAt: { ...timestampColumn },
previousFullVersionEndTime: { ...timestampColumn },
newFullVersionEndTime: { ...timestampColumn }
+3 -2
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
* Copyright (C) 2019 - 2026 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
@@ -82,7 +82,8 @@ export async function createFamily ({
fullVersionUntil: (Date.now() + 1000 * 60 * 60 * 24 * 14).toString(10),
hasFullVersion: true,
nextServerKeyRequestSeq: '1',
u2fKeysVersion: generateVersionId()
u2fKeysVersion: generateVersionId(),
fullVersionDebts: '0'
}, { transaction })
// create parent user
+38 -6
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
* Copyright (C) 2019 - 2026 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
@@ -21,13 +21,14 @@ import { notifyClientsAboutChangesDelayed } from '../../function/websocket'
import { WebsocketApi } from '../../websocket'
const day = 1000 * 60 * 60 * 24
const week = day * 7
const month = day * 31
const year = day * 366
export const addPurchase = async ({ database, familyId, type, service, transactionId, websocket, transaction }: {
database: Database
familyId: string
type: 'month' | 'year'
type: 'month' | 'year' | 'unpaid14'
service: 'googleplay' | 'directpurchase'
transactionId: string
websocket: WebsocketApi
@@ -57,11 +58,42 @@ export const addPurchase = async ({ database, familyId, type, service, transacti
}
const previousFullVersionEndTime = familyEntry.fullVersionUntil
const previousFullVersionDebts = parseInt(familyEntry.fullVersionDebts, 10)
const newFullVersionUntil = Math.max(parseInt(familyEntry.fullVersionUntil, 10), Date.now()) + (type === 'year' ? year : month)
if (type === 'month' || type === 'year') {
const typeDuration = type === 'year' ? year : month
familyEntry.fullVersionUntil = newFullVersionUntil.toString(10)
familyEntry.hasFullVersion = true
if (typeDuration > previousFullVersionDebts) {
const newFullVersionUntil = Math.max(parseInt(familyEntry.fullVersionUntil, 10), Date.now()) + typeDuration - previousFullVersionDebts
familyEntry.fullVersionUntil = newFullVersionUntil.toString(10)
familyEntry.fullVersionDebts = '0'
familyEntry.hasFullVersion = true
} else {
familyEntry.fullVersionDebts = (previousFullVersionDebts - typeDuration).toString(10)
}
} else if (type === 'unpaid14') {
const debtsAdd = 2 * week
const debtsMax = 3 * week
const newDebts = Math.min(debtsMax, previousFullVersionDebts + debtsAdd)
if (newDebts <= previousFullVersionDebts) {
// do not save anything
return
}
const durationToAdd = newDebts - previousFullVersionDebts
const newFullVersionUntil = Math.max(parseInt(familyEntry.fullVersionUntil, 10), Date.now()) + durationToAdd
familyEntry.fullVersionUntil = newFullVersionUntil.toString(10)
familyEntry.fullVersionDebts = newDebts.toString(10)
familyEntry.hasFullVersion = true
} else {
throw new Error()
}
await familyEntry.save({ transaction })
@@ -72,7 +104,7 @@ export const addPurchase = async ({ database, familyId, type, service, transacti
type,
loggedAt: Date.now().toString(10),
previousFullVersionEndTime,
newFullVersionEndTime: newFullVersionUntil.toString(10)
newFullVersionEndTime: familyEntry.fullVersionUntil
}, {
transaction
})
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 Jonas Lochmann
* Copyright (C) 2019 - 2026 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
@@ -15,6 +15,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const canDoNextPurchase = ({ fullVersionUntil }: {fullVersionUntil: number}) => (
fullVersionUntil < (Date.now() + 1000 * 60 * 60 * 24 * 31) // 31 days
export const canDoNextPurchase = ({ fullVersionUntil, fullVersionDebts }: {
fullVersionUntil: number
fullVersionDebts: number
}) => (
(fullVersionUntil - fullVersionDebts) < (Date.now() + 1000 * 60 * 60 * 24 * 31) // 31 days
)
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2020 Jonas Lochmann
* Copyright (C) 2019 - 2026 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
@@ -43,7 +43,7 @@ export const requireFamilyEntry = async ({ database, deviceAuthToken, transactio
where: {
familyId: deviceEntry.familyId
},
attributes: ['fullVersionUntil'],
attributes: ['fullVersionUntil', 'fullVersionDebts'],
transaction
})
@@ -52,7 +52,8 @@ export const requireFamilyEntry = async ({ database, deviceAuthToken, transactio
}
const familyEntry = {
fullVersionUntil: familyEntryUnsafe.fullVersionUntil
fullVersionUntil: familyEntryUnsafe.fullVersionUntil,
fullVersionDebts: familyEntryUnsafe.fullVersionDebts,
}
return familyEntry
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2024 Jonas Lochmann
* Copyright (C) 2019 - 2026 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
@@ -67,6 +67,9 @@ export async function dispatchUpdateTimelimitRule ({
const wasSessionDurationLimitationEnabled =
ruleEntry.sessionPauseMilliseconds > 0 && ruleEntry.sessionDurationMilliseconds > 0
const isSessionDurationLimitationEnabled =
action.sessionPauseMilliseconds > 0 && action.sessionDurationMilliseconds > 0
const countOldAffectedDays = Array(7)
.fill(0)
.reduce((sum, _, index) => sum + ((ruleEntry.dayMaskAsBitmask >> index) & 1), 0)
@@ -78,6 +81,7 @@ export async function dispatchUpdateTimelimitRule ({
action.start <= ruleEntry.startMinuteOfDay &&
action.end >= ruleEntry.endMinuteOfDay &&
(!wasSessionDurationLimitationEnabled || (
isSessionDurationLimitationEnabled &&
action.sessionDurationMilliseconds <= ruleEntry.sessionDurationMilliseconds &&
action.sessionPauseMilliseconds >= ruleEntry.sessionPauseMilliseconds
)) &&
@@ -0,0 +1,64 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2026 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 { createHash } from 'crypto'
const updateMessage =
'Please update the TimeLimit App to the latest release. ' +
'In your current version, synchronization and purchases may not work as expected.' +
'\n\n' +
'Bitte die TimeLimit-App aktualisieren. ' +
'Mit der momentan installierten Version kann es zu Einschränkungen bei der Synchronisation und bei Käufen kommen.'
export function getCampaign({ familyId, isClient750OrNewer }: {
familyId: string
isClient750OrNewer: boolean
}): string | undefined {
const now = Date.now()
const campaigns = [
{
enable: !isClient750OrNewer,
seed: 'Q05NwM4nyUfiYg6K', // require('crypto').randomBytes(12).toString('base64')
startTime: 1784160000000, // new Date('2026-07-16').valueOf()
endTime: 1785369600000, // new Date('2026-07-30').valueOf()
startPercentage: 0,
endPercentage: 10,
message: updateMessage,
}
]
for (const campaign of campaigns) {
if (!campaign.enable) continue
if (now < campaign.startTime) continue
const progress = Math.min((now - campaign.startTime) / (campaign.endTime - campaign.startTime), 1)
const chanceInPercent = campaign.startPercentage + progress * (campaign.endPercentage - campaign.startPercentage)
const hasher = createHash('SHA3-224')
hasher.update(campaign.seed, 'base64')
hasher.update(familyId)
const hash = hasher.digest().readUInt32BE(0)
const match = hash < chanceInPercent * Math.pow(2, 32) / 100
if (match) return campaign.message
}
return undefined
}
@@ -22,6 +22,7 @@ import { getStatusMessage } from '../../../function/statusmessage'
import { ClientDataStatus } from '../../../object/clientdatastatus'
import { ServerDataStatus } from '../../../object/serverdatastatus'
import { EventHandler } from '../../../monitoring/eventhandler'
import { getCampaign } from './campaign'
import {
getCategoryAssignedApps, getCategoryBaseDatas, getCategoryDataToSync,
getRules, getTasks, getUsedTimes
@@ -54,12 +55,18 @@ export const generateServerDataStatus = async ({
const doesClientSupportDh = clientLevel >= 5
const doesClientSupportU2f = clientLevel >= 6
const doesClientSupportPing = clientLevel >= 7
const isClient750OrNewer = clientLevel >= 8 // first release in the post gplay time
const message: string | undefined =
await getStatusMessage({ database, transaction }) ||
getCampaign({ familyId, isClient750OrNewer }) ||
undefined
const result: ServerDataStatus = {
fullVersion: config.alwaysPro ? 1 : (
familyEntry.hasFullVersion ? parseInt(familyEntry.fullVersionUntil, 10) : 0
),
message: await getStatusMessage({ database, transaction }) || undefined,
message,
apiLevel: 9
}
+6 -3
View File
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
* Copyright (C) 2019 - 2026 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
@@ -33,12 +33,15 @@ export async function createIdentityToken({ purpose, familyId, userId, mail }: I
.join('\n')
}
export async function verifyIdentitifyToken(token: string): Promise<IdentityTokenPayload> {
export async function verifyIdentitifyToken(token: string, dryRun: boolean): Promise<IdentityTokenPayload> {
try {
const { payload } = await jwtVerify(
Buffer.from(token, 'base64').toString('ascii'),
getSignSecret(),
{ algorithms: ['HS512'] }
{
algorithms: ['HS512'],
clockTolerance: dryRun ? 0 : '4w',
}
)
if (!isIdentityTokenPayload(payload)) throw new BadPayloadException()