From a165231c475f3dfcb5c5fc3eb341d28ae9ce13f3 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Wed, 22 Jan 2025 11:57:19 +0100 Subject: [PATCH] jwt gen & rename fixes --- .../club/member/memberCommandHandler.ts | 12 ++++-- src/command/user/webapi/webapiCommand.ts | 4 ++ .../user/webapi/webapiCommandHandler.ts | 37 +++++++++++++++++-- .../user/webapi/webapiPermissionCommand.ts | 6 +-- .../webapi/webapiPermissionCommandHandler.ts | 12 +++--- src/controller/admin/user/webapiController.ts | 20 +++++----- src/controller/webapiController.ts | 9 +++-- src/entity/user/webapi.ts | 6 +-- src/helpers/jwtHelper.ts | 17 +++++++-- .../1737453096674-addwebapiTokens.ts | 6 +-- src/routes/admin/user/webapi.ts | 5 +++ src/service/user/webapiPermissionService.ts | 6 +-- src/service/user/webapiService.ts | 2 +- 13 files changed, 101 insertions(+), 41 deletions(-) diff --git a/src/command/club/member/memberCommandHandler.ts b/src/command/club/member/memberCommandHandler.ts index d0fd59a..2bea5b5 100644 --- a/src/command/club/member/memberCommandHandler.ts +++ b/src/command/club/member/memberCommandHandler.ts @@ -33,7 +33,10 @@ export default abstract class MemberCommandHandler { return result.identifiers[0].id; }) .catch((err) => { - throw new InternalException("Failed creating member", err); + throw new InternalException( + `Failed creating member${err.code.includes("ER_DUP_ENTRY") ? " due to duplicate entry for column" : ""}`, + err + ); }); } @@ -58,7 +61,10 @@ export default abstract class MemberCommandHandler { .execute() .then(() => {}) .catch((err) => { - throw new InternalException("Failed updating member", err); + throw new InternalException( + `Failed updating member${err.code.includes("ER_DUP_ENTRY") ? " due to duplicate entry for column" : ""}`, + err + ); }); } @@ -83,7 +89,7 @@ export default abstract class MemberCommandHandler { .execute() .then(() => {}) .catch((err) => { - throw new InternalException("Failed updating member", err); + throw new InternalException(`Failed updating member`, err); }); } diff --git a/src/command/user/webapi/webapiCommand.ts b/src/command/user/webapi/webapiCommand.ts index 2ec3586..4415747 100644 --- a/src/command/user/webapi/webapiCommand.ts +++ b/src/command/user/webapi/webapiCommand.ts @@ -10,6 +10,10 @@ export interface UpdateWebapiCommand { expiry?: Date; } +export interface UpdateLastUsageWebapiCommand { + id: number; +} + export interface DeleteWebapiCommand { id: number; } diff --git a/src/command/user/webapi/webapiCommandHandler.ts b/src/command/user/webapi/webapiCommandHandler.ts index 71689a6..b49f7a6 100644 --- a/src/command/user/webapi/webapiCommandHandler.ts +++ b/src/command/user/webapi/webapiCommandHandler.ts @@ -1,7 +1,12 @@ import { dataSource } from "../../../data-source"; import { webapi } from "../../../entity/user/webapi"; import InternalException from "../../../exceptions/internalException"; -import { CreateWebapiCommand, DeleteWebapiCommand, UpdateWebapiCommand } from "./webapiCommand"; +import { + CreateWebapiCommand, + DeleteWebapiCommand, + UpdateLastUsageWebapiCommand, + UpdateWebapiCommand, +} from "./webapiCommand"; export default abstract class WebapiCommandHandler { /** @@ -24,7 +29,10 @@ export default abstract class WebapiCommandHandler { return result.identifiers[0].token; }) .catch((err) => { - throw new InternalException("Failed creating api", err); + throw new InternalException( + `Failed creating api${err.code.includes("ER_DUP_ENTRY") ? " due to duplicate entry for column" : ""}`, + err + ); }); } @@ -45,7 +53,30 @@ export default abstract class WebapiCommandHandler { .execute() .then(() => {}) .catch((err) => { - throw new InternalException("Failed updating api", err); + throw new InternalException( + `Failed updating api${err.code.includes("ER_DUP_ENTRY") ? " due to duplicate entry for column" : ""}`, + err + ); + }); + } + + /** + * @description update api usage + * @param {UpdateLastUsageWebapiCommand} updateWebapi + * @returns {Promise} + */ + static async updateUsage(updateWebapi: UpdateLastUsageWebapiCommand): Promise { + return await dataSource + .createQueryBuilder() + .update(webapi) + .set({ + lastUsage: new Date(), + }) + .where("id = :id", { id: updateWebapi.id }) + .execute() + .then(() => {}) + .catch((err) => { + throw new InternalException(`Failed updating api last usage`, err); }); } diff --git a/src/command/user/webapi/webapiPermissionCommand.ts b/src/command/user/webapi/webapiPermissionCommand.ts index 6f192d1..3942115 100644 --- a/src/command/user/webapi/webapiPermissionCommand.ts +++ b/src/command/user/webapi/webapiPermissionCommand.ts @@ -2,15 +2,15 @@ import { PermissionString } from "../../../type/permissionTypes"; export interface CreateWebapiPermissionCommand { permission: PermissionString; - apiId: number; + webapiId: number; } export interface DeleteWebapiPermissionCommand { permission: PermissionString; - apiId: number; + webapiId: number; } export interface UpdateWebapiPermissionsCommand { - apiId: number; + webapiId: number; permissions: Array; } diff --git a/src/command/user/webapi/webapiPermissionCommandHandler.ts b/src/command/user/webapi/webapiPermissionCommandHandler.ts index 6d30bc0..74ea514 100644 --- a/src/command/user/webapi/webapiPermissionCommandHandler.ts +++ b/src/command/user/webapi/webapiPermissionCommandHandler.ts @@ -19,7 +19,7 @@ export default abstract class WebapiPermissionCommandHandler { * @returns {Promise} */ static async updatePermissions(updateWebapiPermissions: UpdateWebapiPermissionsCommand): Promise { - let currentPermissions = (await WebapiPermissionService.getByApi(updateWebapiPermissions.apiId)).map( + let currentPermissions = (await WebapiPermissionService.getByApi(updateWebapiPermissions.webapiId)).map( (r) => r.permission ); return await dataSource.manager @@ -30,10 +30,10 @@ export default abstract class WebapiPermissionCommandHandler { updateWebapiPermissions.permissions ); if (newPermissions.length != 0) { - await this.updatePermissionsAdd(manager, updateWebapiPermissions.apiId, newPermissions); + await this.updatePermissionsAdd(manager, updateWebapiPermissions.webapiId, newPermissions); } if (removePermissions.length != 0) { - await this.updatePermissionsRemove(manager, updateWebapiPermissions.apiId, removePermissions); + await this.updatePermissionsRemove(manager, updateWebapiPermissions.webapiId, removePermissions); } }) .then(() => {}) @@ -54,7 +54,7 @@ export default abstract class WebapiPermissionCommandHandler { .values( permissions.map((p) => ({ permission: p, - apiId: webapiId, + webapiId: webapiId, })) ) .orIgnore() @@ -87,7 +87,7 @@ export default abstract class WebapiPermissionCommandHandler { .into(webapiPermission) .values({ permission: createPermission.permission, - webapiId: createPermission.apiId, + webapiId: createPermission.webapiId, }) .execute() .then((result) => { @@ -108,7 +108,7 @@ export default abstract class WebapiPermissionCommandHandler { .createQueryBuilder() .delete() .from(webapiPermission) - .where("webapiId = :id", { id: deletePermission.apiId }) + .where("webapiId = :id", { id: deletePermission.webapiId }) .andWhere("permission = :permission", { permission: deletePermission.permission }) .execute() .then(() => {}) diff --git a/src/controller/admin/user/webapiController.ts b/src/controller/admin/user/webapiController.ts index bcef24b..d5390ff 100644 --- a/src/controller/admin/user/webapiController.ts +++ b/src/controller/admin/user/webapiController.ts @@ -74,14 +74,16 @@ export async function getWebapiPermissions(req: Request, res: Response): Promise */ export async function createWebapi(req: Request, res: Response): Promise { let title = req.body.title; - let expiry = req.body.expiry; + let expiry = req.body.expiry || null; - let token = await JWTHelper.create({ - iss: CLUB_NAME, - sub: "api_token_retrieve", - iat: new Date().toISOString(), - aud: StringHelper.random(32), - }); + let token = await JWTHelper.create( + { + iss: CLUB_NAME, + sub: "api_token_retrieve", + aud: StringHelper.random(32), + }, + { useExpiration: false } + ); let createApi: CreateWebapiCommand = { token: token, @@ -102,7 +104,7 @@ export async function createWebapi(req: Request, res: Response): Promise { export async function updateWebapi(req: Request, res: Response): Promise { const id = parseInt(req.params.id); let title = req.body.title; - let expiry = req.body.expiry; + let expiry = req.body.expiry || null; let updateApi: UpdateWebapiCommand = { id: id, @@ -127,7 +129,7 @@ export async function updateWebapiPermissions(req: Request, res: Response): Prom let permissionStrings = PermissionHelper.convertToStringArray(permissions); let updateApiPermissions: UpdateWebapiPermissionsCommand = { - apiId: id, + webapiId: id, permissions: permissionStrings, }; await WebapiPermissionCommandHandler.updatePermissions(updateApiPermissions); diff --git a/src/controller/webapiController.ts b/src/controller/webapiController.ts index 33f765d..7af86dd 100644 --- a/src/controller/webapiController.ts +++ b/src/controller/webapiController.ts @@ -10,6 +10,7 @@ import UnauthorizedRequestException from "../exceptions/unauthorizedRequestExcep import RefreshService from "../service/refreshService"; import WebapiService from "../service/user/webapiService"; import ForbiddenRequestException from "../exceptions/forbiddenRequestException"; +import WebapiCommandHandler from "../command/user/webapi/webapiCommandHandler"; /** * @description Check authentication status by token @@ -20,13 +21,15 @@ import ForbiddenRequestException from "../exceptions/forbiddenRequestException"; export async function getWebApiAccess(req: Request, res: Response): Promise { const bearer = req.headers.authorization?.split(" ")?.[1] ?? undefined; - let { expiry } = await WebapiService.getByToken(bearer); + let { id, expiry } = await WebapiService.getByToken(bearer); - if (new Date() > new Date(expiry)) { + if (expiry != null && new Date() > new Date(expiry)) { throw new ForbiddenRequestException("api token expired"); } - let accessToken = await JWTHelper.buildWebapiToken(bearer); + await WebapiCommandHandler.updateUsage({ id }); + + let accessToken = await JWTHelper.buildWebapiToken(bearer, expiry); res.json({ accessToken, diff --git a/src/entity/user/webapi.ts b/src/entity/user/webapi.ts index e1c73a6..220db8c 100644 --- a/src/entity/user/webapi.ts +++ b/src/entity/user/webapi.ts @@ -6,10 +6,10 @@ export class webapi { @PrimaryColumn({ generated: "increment", type: "int" }) id: number; - @Column({ type: "varchar", length: 255, unique: true, select: false }) + @Column({ type: "text", unique: true, select: false }) token: string; - @Column({ type: "varchar", length: 255 }) + @Column({ type: "varchar", length: 255, unique: true }) title: string; @CreateDateColumn() @@ -18,7 +18,7 @@ export class webapi { @Column({ type: "datetime", nullable: true }) lastUsage?: Date; - @Column({ type: "datetime", nullable: true }) + @Column({ type: "date", nullable: true }) expiry?: Date; @OneToMany(() => webapiPermission, (apiPermission) => apiPermission.webapi) diff --git a/src/helpers/jwtHelper.ts b/src/helpers/jwtHelper.ts index af177e5..791af2a 100644 --- a/src/helpers/jwtHelper.ts +++ b/src/helpers/jwtHelper.ts @@ -8,6 +8,7 @@ import UserService from "../service/user/userService"; import PermissionHelper from "./permissionHelper"; import WebapiService from "../service/user/webapiService"; import WebapiPermissionService from "../service/user/webapiPermissionService"; +import ms from "ms"; export abstract class JWTHelper { static validate(token: string): Promise { @@ -19,13 +20,16 @@ export abstract class JWTHelper { }); } - static create(data: JWTData): Promise { + static create( + data: JWTData, + { expOverwrite, useExpiration }: { expOverwrite?: number; useExpiration?: boolean } = { useExpiration: true } + ): Promise { return new Promise((resolve, reject) => { jwt.sign( data, JWT_SECRET, { - expiresIn: JWT_EXPIRATION, + ...(useExpiration ?? true ? { expiresIn: expOverwrite ?? JWT_EXPIRATION } : {}), }, (err, token) => { if (err) reject(err.message); @@ -75,7 +79,7 @@ export abstract class JWTHelper { }); } - static async buildWebapiToken(token: string): Promise { + static async buildWebapiToken(token: string, expiration?: Date): Promise { let { id, title } = await WebapiService.getByToken(token); let webapiPermissions = await WebapiPermissionService.getByApi(id); let webapiPermissionStrings = webapiPermissions.map((e) => e.permission); @@ -92,7 +96,12 @@ export abstract class JWTHelper { sub: "webapi_access_token", }; - return await JWTHelper.create(jwtData) + let overwriteExpiration = + ms(JWT_EXPIRATION) < new Date().getTime() - new Date(expiration).getTime() + ? null + : Date.now() - new Date(expiration).getTime(); + + return await JWTHelper.create(jwtData, { expOverwrite: overwriteExpiration, useExpiration: true }) .then((result) => { return result; }) diff --git a/src/migrations/1737453096674-addwebapiTokens.ts b/src/migrations/1737453096674-addwebapiTokens.ts index 81b4e45..55f37fb 100644 --- a/src/migrations/1737453096674-addwebapiTokens.ts +++ b/src/migrations/1737453096674-addwebapiTokens.ts @@ -12,11 +12,11 @@ export class AddWebapiTokens1737453096674 implements MigrationInterface { name: "webapi", columns: [ { name: "id", type: variableType_int, isPrimary: true, isGenerated: true, generationStrategy: "increment" }, - { name: "token", type: "varchar", length: "255", isUnique: true, isNullable: false }, - { name: "title", type: "varchar", length: "255", isNullable: false }, + { name: "token", type: "text", isUnique: true, isNullable: false }, + { name: "title", type: "varchar", isUnique: true, length: "255", isNullable: false }, { name: "createdAt", type: "datetime", default: "CURRENT_TIMESTAMP(6)", isNullable: false }, { name: "lastUsage", type: "datetime", isNullable: true, default: null }, - { name: "expiry", type: "datetime", isNullable: true, default: null }, + { name: "expiry", type: "date", isNullable: true, default: null }, ], }), true diff --git a/src/routes/admin/user/webapi.ts b/src/routes/admin/user/webapi.ts index 4e8feb9..0d0e7ab 100644 --- a/src/routes/admin/user/webapi.ts +++ b/src/routes/admin/user/webapi.ts @@ -6,6 +6,7 @@ import { getAllWebapis, getWebapiById, getWebapiPermissions, + getWebapiTokenById, updateWebapi, updateWebapiPermissions, } from "../../../controller/admin/user/webapiController"; @@ -20,6 +21,10 @@ router.get("/:id", async (req: Request, res: Response) => { await getWebapiById(req, res); }); +router.get("/:id/token", async (req: Request, res: Response) => { + await getWebapiTokenById(req, res); +}); + router.get("/:id/permissions", async (req: Request, res: Response) => { await getWebapiPermissions(req, res); }); diff --git a/src/service/user/webapiPermissionService.ts b/src/service/user/webapiPermissionService.ts index c50895c..a210353 100644 --- a/src/service/user/webapiPermissionService.ts +++ b/src/service/user/webapiPermissionService.ts @@ -11,14 +11,14 @@ export default abstract class WebapiPermissionService { static async getByApi(webapiId: number): Promise> { return await dataSource .getRepository(webapiPermission) - .createQueryBuilder("api_permission") - .where("api_permission.apiId = :apiId", { apiId: webapiId }) + .createQueryBuilder("webapi_permission") + .where("webapi_permission.webapiId = :webapiId", { webapiId: webapiId }) .getMany() .then((res) => { return res; }) .catch((err) => { - throw new InternalException("api permissions not found by api", err); + throw new InternalException("webapi permissions not found by api", err); }); } } diff --git a/src/service/user/webapiService.ts b/src/service/user/webapiService.ts index 264ef7a..0faa51a 100644 --- a/src/service/user/webapiService.ts +++ b/src/service/user/webapiService.ts @@ -70,7 +70,7 @@ export default abstract class WebapiService { return await dataSource .getRepository(webapi) .createQueryBuilder("webapi") - .select("token") + .select("webapi.token") .where("webapi.id = :id", { id: id }) .getOneOrFail() .then((res) => {