timelimit-server/src/api/admin.ts
2022-09-26 02:00:00 +02:00

283 lines
7.8 KiB
TypeScript

/*
* 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 { json } from 'body-parser'
import { Router } from 'express'
import { BadRequest, Conflict } from 'http-errors'
import * as Sequelize from 'sequelize'
import { Database } from '../database'
import { addPurchase, canDoNextPurchase } from '../function/purchase'
import { getStatusMessage, setStatusMessage } from '../function/statusmessage'
import { EventHandler } from '../monitoring/eventhandler'
import { generatePurchaseId } from '../util/token'
import { verifyIdentitifyToken, TokenValidationException } from '../util/identity-token'
import { WebsocketApi } from '../websocket'
export const createAdminRouter = ({ database, websocket, eventHandler }: {
database: Database
websocket: WebsocketApi
eventHandler: EventHandler
}) => {
const router = Router()
router.get('/status', async (_, res, next) => {
try {
const { counters, maxValues } = await eventHandler.getValues()
res.json({
websocketClients: websocket.countConnections(),
counters,
maxValues
})
} catch (ex) {
next(ex)
}
})
router.post('/reset-counters', async (_, res, next) => {
try {
await eventHandler.reset()
res.json({ ok: true })
} catch (ex) {
next(ex)
}
})
router.get('/status-message', async (_, res, next) => {
try {
const currentStatusMessage = await getStatusMessage({ database })
res.json({
statusMessage: currentStatusMessage
})
} catch (ex) {
next(ex)
}
})
router.post('/status-message', json(), async (req, res, next) => {
try {
if (typeof req.body !== 'object' || typeof req.body.message !== 'string') {
throw new BadRequest()
}
const newStatusMessage = req.body.message as string
await setStatusMessage({ database, newStatusMessage })
websocket.triggerImportantSyncAtAllDevicesInBackground()
res.json({ ok: true })
} catch (ex) {
next(ex)
}
})
router.post('/unlock-premium', json(), async (req, res, next) => {
try {
if (typeof req.body !== 'object' || typeof req.body.mail !== 'string' || typeof req.body.duration !== 'string') {
throw new BadRequest()
}
const mail: string = req.body.mail
const type: string = req.body.duration
if (type !== 'month' && type !== 'year' || mail === '') {
throw new BadRequest()
}
await database.transaction(async (transaction) => {
const userEntryUnsafe = await database.user.findOne({
where: {
mail
},
attributes: ['familyId'],
transaction
})
if (!userEntryUnsafe) {
throw new Conflict('no user with specified mail address')
}
const userEntry = {
familyId: userEntryUnsafe.familyId
}
await addPurchase({
database,
familyId: userEntry.familyId,
type,
service: 'directpurchase',
transactionId: 'legacyunlock-' + type + '-' + generatePurchaseId(),
websocket,
transaction
})
})
res.json({ ok: true })
} catch (ex) {
next(ex)
}
})
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
}