Merge pull request 'feature/#22-API-Tokens' (#49) from feature/#22-API-Tokens into develop
Reviewed-on: #49
This commit is contained in:
commit
bbfdc69049
28 changed files with 886 additions and 9 deletions
|
@ -18,5 +18,5 @@ MAIL_HOST = mail_hoststring
|
|||
MAIL_PORT = mail_portnumber
|
||||
MAIL_SECURE (true|false) // true for port 465, fals for other ports
|
||||
|
||||
CLUB_NAME = clubname
|
||||
CLUB_NAME = clubname #default FF Admin
|
||||
CLUB_WEBSITE = https://my-club-website-url
|
|
@ -40,7 +40,7 @@ services:
|
|||
- MAIL_HOST=<url>
|
||||
- MAIL_PORT=<port> # default ist auf 578 gesetzt
|
||||
- MAIL_SECURE=<boolean> # default ist auf false gesetzt
|
||||
- CLUB_NAME=<tobemodified>
|
||||
- CLUB_NAME=<tobemodified> # default ist auf FF Admin gesetzt
|
||||
- CLUB_WEBSITE=<tobemodified>
|
||||
volumes:
|
||||
- <volume|local path>:/app/files
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
19
src/command/user/webapi/webapiCommand.ts
Normal file
19
src/command/user/webapi/webapiCommand.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
export interface CreateWebapiCommand {
|
||||
title: string;
|
||||
token: string;
|
||||
expiry?: Date;
|
||||
}
|
||||
|
||||
export interface UpdateWebapiCommand {
|
||||
id: number;
|
||||
title: string;
|
||||
expiry?: Date;
|
||||
}
|
||||
|
||||
export interface UpdateLastUsageWebapiCommand {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface DeleteWebapiCommand {
|
||||
id: number;
|
||||
}
|
100
src/command/user/webapi/webapiCommandHandler.ts
Normal file
100
src/command/user/webapi/webapiCommandHandler.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
import { dataSource } from "../../../data-source";
|
||||
import { webapi } from "../../../entity/user/webapi";
|
||||
import InternalException from "../../../exceptions/internalException";
|
||||
import {
|
||||
CreateWebapiCommand,
|
||||
DeleteWebapiCommand,
|
||||
UpdateLastUsageWebapiCommand,
|
||||
UpdateWebapiCommand,
|
||||
} from "./webapiCommand";
|
||||
|
||||
export default abstract class WebapiCommandHandler {
|
||||
/**
|
||||
* @description create api
|
||||
* @param {CreateWebapiCommand} createWebapi
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
static async create(createWebapi: CreateWebapiCommand): Promise<number> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(webapi)
|
||||
.values({
|
||||
token: createWebapi.token,
|
||||
title: createWebapi.title,
|
||||
expiry: createWebapi.expiry,
|
||||
})
|
||||
.execute()
|
||||
.then((result) => {
|
||||
return result.identifiers[0].token;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException(
|
||||
`Failed creating api${err.code.includes("ER_DUP_ENTRY") ? " due to duplicate entry for column" : ""}`,
|
||||
err
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description update api
|
||||
* @param {UpdateWebapiCommand} updateWebapi
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async update(updateWebapi: UpdateWebapiCommand): Promise<void> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.update(webapi)
|
||||
.set({
|
||||
title: updateWebapi.title,
|
||||
expiry: updateWebapi.expiry,
|
||||
})
|
||||
.where("id = :id", { id: updateWebapi.id })
|
||||
.execute()
|
||||
.then(() => {})
|
||||
.catch((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<void>}
|
||||
*/
|
||||
static async updateUsage(updateWebapi: UpdateLastUsageWebapiCommand): Promise<void> {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description delete api
|
||||
* @param {DeleteWebapiCommand} deleteWebapi
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async delete(deleteWebapi: DeleteWebapiCommand): Promise<void> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(webapi)
|
||||
.where("id = :id", { id: deleteWebapi.id })
|
||||
.execute()
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
throw new InternalException("Failed deleting api", err);
|
||||
});
|
||||
}
|
||||
}
|
16
src/command/user/webapi/webapiPermissionCommand.ts
Normal file
16
src/command/user/webapi/webapiPermissionCommand.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { PermissionString } from "../../../type/permissionTypes";
|
||||
|
||||
export interface CreateWebapiPermissionCommand {
|
||||
permission: PermissionString;
|
||||
webapiId: number;
|
||||
}
|
||||
|
||||
export interface DeleteWebapiPermissionCommand {
|
||||
permission: PermissionString;
|
||||
webapiId: number;
|
||||
}
|
||||
|
||||
export interface UpdateWebapiPermissionsCommand {
|
||||
webapiId: number;
|
||||
permissions: Array<PermissionString>;
|
||||
}
|
119
src/command/user/webapi/webapiPermissionCommandHandler.ts
Normal file
119
src/command/user/webapi/webapiPermissionCommandHandler.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
import { DeleteResult, EntityManager, InsertResult } from "typeorm";
|
||||
import { dataSource } from "../../../data-source";
|
||||
import { webapiPermission } from "../../../entity/user/webapi_permission";
|
||||
import InternalException from "../../../exceptions/internalException";
|
||||
import WebapiService from "../../../service/user/webapiService";
|
||||
import {
|
||||
CreateWebapiPermissionCommand,
|
||||
DeleteWebapiPermissionCommand,
|
||||
UpdateWebapiPermissionsCommand,
|
||||
} from "./webapiPermissionCommand";
|
||||
import PermissionHelper from "../../../helpers/permissionHelper";
|
||||
import WebapiPermissionService from "../../../service/user/webapiPermissionService";
|
||||
import { PermissionString } from "../../../type/permissionTypes";
|
||||
|
||||
export default abstract class WebapiPermissionCommandHandler {
|
||||
/**
|
||||
* @description update api permissions
|
||||
* @param {UpdateWebapiPermissionsCommand} updateWebapiPermissions
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async updatePermissions(updateWebapiPermissions: UpdateWebapiPermissionsCommand): Promise<void> {
|
||||
let currentPermissions = (await WebapiPermissionService.getByApi(updateWebapiPermissions.webapiId)).map(
|
||||
(r) => r.permission
|
||||
);
|
||||
return await dataSource.manager
|
||||
.transaction(async (manager) => {
|
||||
let newPermissions = PermissionHelper.getWhatToAdd(currentPermissions, updateWebapiPermissions.permissions);
|
||||
let removePermissions = PermissionHelper.getWhatToRemove(
|
||||
currentPermissions,
|
||||
updateWebapiPermissions.permissions
|
||||
);
|
||||
if (newPermissions.length != 0) {
|
||||
await this.updatePermissionsAdd(manager, updateWebapiPermissions.webapiId, newPermissions);
|
||||
}
|
||||
if (removePermissions.length != 0) {
|
||||
await this.updatePermissionsRemove(manager, updateWebapiPermissions.webapiId, removePermissions);
|
||||
}
|
||||
})
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
throw new InternalException("Failed saving api permissions", err);
|
||||
});
|
||||
}
|
||||
|
||||
private static async updatePermissionsAdd(
|
||||
manager: EntityManager,
|
||||
webapiId: number,
|
||||
permissions: Array<PermissionString>
|
||||
): Promise<InsertResult> {
|
||||
return await manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(webapiPermission)
|
||||
.values(
|
||||
permissions.map((p) => ({
|
||||
permission: p,
|
||||
webapiId: webapiId,
|
||||
}))
|
||||
)
|
||||
.orIgnore()
|
||||
.execute();
|
||||
}
|
||||
|
||||
private static async updatePermissionsRemove(
|
||||
manager: EntityManager,
|
||||
webapiId: number,
|
||||
permissions: Array<PermissionString>
|
||||
): Promise<DeleteResult> {
|
||||
return await manager
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(webapiPermission)
|
||||
.where("webapiId = :id", { id: webapiId })
|
||||
.andWhere("permission IN (:...permission)", { permission: permissions })
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description grant permission to user
|
||||
* @param {CreateWebapiPermissionCommand} createPermission
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
static async create(createPermission: CreateWebapiPermissionCommand): Promise<number> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(webapiPermission)
|
||||
.values({
|
||||
permission: createPermission.permission,
|
||||
webapiId: createPermission.webapiId,
|
||||
})
|
||||
.execute()
|
||||
.then((result) => {
|
||||
return result.identifiers[0].id;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException("Failed saving api permission", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description remove permission from api
|
||||
* @param {DeleteWebapiPermissionCommand} deletePermission
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
static async delete(deletePermission: DeleteWebapiPermissionCommand): Promise<any> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(webapiPermission)
|
||||
.where("webapiId = :id", { id: deletePermission.webapiId })
|
||||
.andWhere("permission = :permission", { permission: deletePermission.permission })
|
||||
.execute()
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
throw new InternalException("failed api permission removal", err);
|
||||
});
|
||||
}
|
||||
}
|
155
src/controller/admin/user/webapiController.ts
Normal file
155
src/controller/admin/user/webapiController.ts
Normal file
|
@ -0,0 +1,155 @@
|
|||
import { Request, Response } from "express";
|
||||
import WebapiService from "../../../service/user/webapiService";
|
||||
import ApiFactory from "../../../factory/admin/user/webapi";
|
||||
import WebapiPermissionService from "../../../service/user/webapiPermissionService";
|
||||
import PermissionHelper from "../../../helpers/permissionHelper";
|
||||
import {
|
||||
CreateWebapiCommand,
|
||||
DeleteWebapiCommand,
|
||||
UpdateWebapiCommand,
|
||||
} from "../../../command/user/webapi/webapiCommand";
|
||||
import WebapiCommandHandler from "../../../command/user/webapi/webapiCommandHandler";
|
||||
import { UpdateWebapiPermissionsCommand } from "../../../command/user/webapi/webapiPermissionCommand";
|
||||
import WebapiPermissionCommandHandler from "../../../command/user/webapi/webapiPermissionCommandHandler";
|
||||
import { JWTHelper } from "../../../helpers/jwtHelper";
|
||||
import { CLUB_NAME } from "../../../env.defaults";
|
||||
import { StringHelper } from "../../../helpers/stringHelper";
|
||||
|
||||
/**
|
||||
* @description get All apis
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getAllWebapis(req: Request, res: Response): Promise<any> {
|
||||
let apis = await WebapiService.getAll();
|
||||
|
||||
res.json(ApiFactory.mapToBase(apis));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get api by id
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getWebapiById(req: Request, res: Response): Promise<any> {
|
||||
const id = parseInt(req.params.id);
|
||||
let api = await WebapiService.getById(id);
|
||||
|
||||
res.json(ApiFactory.mapToSingle(api));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get api token by id
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getWebapiTokenById(req: Request, res: Response): Promise<any> {
|
||||
const id = parseInt(req.params.id);
|
||||
let { token } = await WebapiService.getTokenById(id);
|
||||
|
||||
res.send(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get permissions by api
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getWebapiPermissions(req: Request, res: Response): Promise<any> {
|
||||
const id = parseInt(req.params.id);
|
||||
let permissions = await WebapiPermissionService.getByApi(id);
|
||||
|
||||
res.json(PermissionHelper.convertToObject(permissions.map((p) => p.permission)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description create new api
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function createWebapi(req: Request, res: Response): Promise<any> {
|
||||
let title = req.body.title;
|
||||
let expiry = req.body.expiry || null;
|
||||
|
||||
let token = await JWTHelper.create(
|
||||
{
|
||||
iss: CLUB_NAME,
|
||||
sub: "api_token_retrieve",
|
||||
aud: StringHelper.random(32),
|
||||
},
|
||||
{ useExpiration: false }
|
||||
);
|
||||
|
||||
let createApi: CreateWebapiCommand = {
|
||||
token: token,
|
||||
title: title,
|
||||
expiry: expiry,
|
||||
};
|
||||
await WebapiCommandHandler.create(createApi);
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description update api data
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function updateWebapi(req: Request, res: Response): Promise<any> {
|
||||
const id = parseInt(req.params.id);
|
||||
let title = req.body.title;
|
||||
let expiry = req.body.expiry || null;
|
||||
|
||||
let updateApi: UpdateWebapiCommand = {
|
||||
id: id,
|
||||
title: title,
|
||||
expiry: expiry,
|
||||
};
|
||||
await WebapiCommandHandler.update(updateApi);
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description update api assigned permission strings
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function updateWebapiPermissions(req: Request, res: Response): Promise<any> {
|
||||
const id = parseInt(req.params.id);
|
||||
let permissions = req.body.permissions;
|
||||
|
||||
let permissionStrings = PermissionHelper.convertToStringArray(permissions);
|
||||
|
||||
let updateApiPermissions: UpdateWebapiPermissionsCommand = {
|
||||
webapiId: id,
|
||||
permissions: permissionStrings,
|
||||
};
|
||||
await WebapiPermissionCommandHandler.updatePermissions(updateApiPermissions);
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description delete api by id
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function deleteWebapi(req: Request, res: Response): Promise<any> {
|
||||
const id = parseInt(req.params.id);
|
||||
|
||||
let deleteApi: DeleteWebapiCommand = {
|
||||
id: id,
|
||||
};
|
||||
await WebapiCommandHandler.delete(deleteApi);
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
37
src/controller/webapiController.ts
Normal file
37
src/controller/webapiController.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { Request, Response } from "express";
|
||||
import { JWTHelper } from "../helpers/jwtHelper";
|
||||
import { JWTToken } from "../type/jwtTypes";
|
||||
import InternalException from "../exceptions/internalException";
|
||||
import RefreshCommandHandler from "../command/refreshCommandHandler";
|
||||
import { CreateRefreshCommand, DeleteRefreshCommand } from "../command/refreshCommand";
|
||||
import UserService from "../service/user/userService";
|
||||
import speakeasy from "speakeasy";
|
||||
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
|
||||
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
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getWebApiAccess(req: Request, res: Response): Promise<any> {
|
||||
const bearer = req.headers.authorization?.split(" ")?.[1] ?? undefined;
|
||||
|
||||
let { id, expiry } = await WebapiService.getByToken(bearer);
|
||||
|
||||
if (expiry != null && new Date() > new Date(expiry)) {
|
||||
throw new ForbiddenRequestException("api token expired");
|
||||
}
|
||||
|
||||
await WebapiCommandHandler.updateUsage({ id });
|
||||
|
||||
let accessToken = await JWTHelper.buildWebapiToken(bearer, expiry);
|
||||
|
||||
res.json({
|
||||
accessToken,
|
||||
});
|
||||
}
|
|
@ -68,6 +68,9 @@ import { Memberlist1736079005086 } from "./migrations/1736079005086-memberlist";
|
|||
import { ExtendViewValues1736084198860 } from "./migrations/1736084198860-extendViewValues";
|
||||
import { FinishInternalIdTransfer1736505324488 } from "./migrations/1736505324488-finishInternalIdTransfer";
|
||||
import { ProtocolPresenceExcuse1737287798828 } from "./migrations/1737287798828-protocolPresenceExcuse";
|
||||
import { webapi } from "./entity/user/webapi";
|
||||
import { webapiPermission } from "./entity/user/webapi_permission";
|
||||
import { AddWebapiTokens1737453096674 } from "./migrations/1737453096674-addwebapiTokens";
|
||||
|
||||
const dataSource = new DataSource({
|
||||
type: DB_TYPE as any,
|
||||
|
@ -117,6 +120,8 @@ const dataSource = new DataSource({
|
|||
memberExecutivePositionsView,
|
||||
memberQualificationsView,
|
||||
membershipView,
|
||||
webapi,
|
||||
webapiPermission,
|
||||
],
|
||||
migrations: [
|
||||
Initial1724317398939,
|
||||
|
@ -146,6 +151,7 @@ const dataSource = new DataSource({
|
|||
ExtendViewValues1736084198860,
|
||||
FinishInternalIdTransfer1736505324488,
|
||||
ProtocolPresenceExcuse1737287798828,
|
||||
AddWebapiTokens1737453096674,
|
||||
],
|
||||
migrationsRun: true,
|
||||
migrationsTransactionMode: "each",
|
||||
|
|
26
src/entity/user/webapi.ts
Normal file
26
src/entity/user/webapi.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { Column, CreateDateColumn, Entity, OneToMany, PrimaryColumn } from "typeorm";
|
||||
import { webapiPermission } from "./webapi_permission";
|
||||
|
||||
@Entity()
|
||||
export class webapi {
|
||||
@PrimaryColumn({ generated: "increment", type: "int" })
|
||||
id: number;
|
||||
|
||||
@Column({ type: "text", unique: true, select: false })
|
||||
token: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255, unique: true })
|
||||
title: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@Column({ type: "datetime", nullable: true })
|
||||
lastUsage?: Date;
|
||||
|
||||
@Column({ type: "date", nullable: true })
|
||||
expiry?: Date;
|
||||
|
||||
@OneToMany(() => webapiPermission, (apiPermission) => apiPermission.webapi)
|
||||
permissions: webapiPermission[];
|
||||
}
|
19
src/entity/user/webapi_permission.ts
Normal file
19
src/entity/user/webapi_permission.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Column, Entity, ManyToOne, OneToMany, PrimaryColumn } from "typeorm";
|
||||
import { PermissionObject, PermissionString } from "../../type/permissionTypes";
|
||||
import { webapi } from "./webapi";
|
||||
|
||||
@Entity()
|
||||
export class webapiPermission {
|
||||
@PrimaryColumn({ type: "int" })
|
||||
webapiId: number;
|
||||
|
||||
@PrimaryColumn({ type: "varchar", length: 255 })
|
||||
permission: PermissionString;
|
||||
|
||||
@ManyToOne(() => webapi, {
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
onUpdate: "RESTRICT",
|
||||
})
|
||||
webapi: webapi;
|
||||
}
|
|
@ -21,7 +21,7 @@ export const MAIL_HOST = process.env.MAIL_HOST ?? "";
|
|||
export const MAIL_PORT = Number(process.env.MAIL_PORT ?? "587");
|
||||
export const MAIL_SECURE = process.env.MAIL_SECURE ?? "false";
|
||||
|
||||
export const CLUB_NAME = process.env.CLUB_NAME ?? "";
|
||||
export const CLUB_NAME = process.env.CLUB_NAME ?? "FF Admin";
|
||||
export const CLUB_WEBSITE = process.env.CLUB_WEBSITE ?? "";
|
||||
|
||||
export function configCheck() {
|
||||
|
|
30
src/factory/admin/user/webapi.ts
Normal file
30
src/factory/admin/user/webapi.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { webapi } from "../../../entity/user/webapi";
|
||||
import PermissionHelper from "../../../helpers/permissionHelper";
|
||||
import { ApiViewModel } from "../../../viewmodel/admin/user/webapi.models";
|
||||
|
||||
export default abstract class ApiFactory {
|
||||
/**
|
||||
* @description map record to api
|
||||
* @param {webapi} record
|
||||
* @returns {apiViewModel}
|
||||
*/
|
||||
public static mapToSingle(record: webapi): ApiViewModel {
|
||||
return {
|
||||
id: record.id,
|
||||
permissions: PermissionHelper.convertToObject(record.permissions.map((e) => e.permission)),
|
||||
title: record.title,
|
||||
expiry: record.expiry,
|
||||
lastUsage: record.lastUsage,
|
||||
createdAt: record.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description map records to api
|
||||
* @param {Array<webapi>} records
|
||||
* @returns {Array<apiViewModel>}
|
||||
*/
|
||||
public static mapToBase(records: Array<webapi>): Array<ApiViewModel> {
|
||||
return records.map((r) => this.mapToSingle(r));
|
||||
}
|
||||
}
|
|
@ -6,6 +6,9 @@ import RolePermissionService from "../service/user/rolePermissionService";
|
|||
import UserPermissionService from "../service/user/userPermissionService";
|
||||
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<string | jwt.JwtPayload> {
|
||||
|
@ -17,13 +20,16 @@ export abstract class JWTHelper {
|
|||
});
|
||||
}
|
||||
|
||||
static create(data: JWTData): Promise<string> {
|
||||
static create(
|
||||
data: JWTData,
|
||||
{ expOverwrite, useExpiration }: { expOverwrite?: number; useExpiration?: boolean } = { useExpiration: true }
|
||||
): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
jwt.sign(
|
||||
data,
|
||||
JWT_SECRET,
|
||||
{
|
||||
expiresIn: JWT_EXPIRATION,
|
||||
...(useExpiration ?? true ? { expiresIn: expOverwrite ?? JWT_EXPIRATION } : {}),
|
||||
},
|
||||
(err, token) => {
|
||||
if (err) reject(err.message);
|
||||
|
@ -72,4 +78,35 @@ export abstract class JWTHelper {
|
|||
throw new InternalException("Failed accessToken creation", err);
|
||||
});
|
||||
}
|
||||
|
||||
static async buildWebapiToken(token: string, expiration?: Date): Promise<string> {
|
||||
let { id, title } = await WebapiService.getByToken(token);
|
||||
let webapiPermissions = await WebapiPermissionService.getByApi(id);
|
||||
let webapiPermissionStrings = webapiPermissions.map((e) => e.permission);
|
||||
let permissionObject = PermissionHelper.convertToObject(webapiPermissionStrings);
|
||||
|
||||
let jwtData: JWTToken = {
|
||||
userId: id,
|
||||
mail: "",
|
||||
username: title,
|
||||
firstname: "",
|
||||
lastname: "",
|
||||
isOwner: false,
|
||||
permissions: permissionObject,
|
||||
sub: "webapi_access_token",
|
||||
};
|
||||
|
||||
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;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException("Failed webapi accessToken creation", err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ declare global {
|
|||
isOwner: boolean;
|
||||
permissions: PermissionObject;
|
||||
isPWA: boolean;
|
||||
isWebApiRequest: boolean;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,10 +29,15 @@ export default async function authenticate(req: Request, res: Response, next: Fu
|
|||
throw new InternalException("process failed");
|
||||
}
|
||||
|
||||
if (decoded?.sub == "api_token_retrieve") {
|
||||
throw new BadRequestException("This token is only authorized to get temporary access tokens via GET /api/webapi");
|
||||
}
|
||||
|
||||
req.userId = decoded.userId;
|
||||
req.username = decoded.username;
|
||||
req.isOwner = decoded.isOwner;
|
||||
req.permissions = decoded.permissions;
|
||||
req.isWebApiRequest = decoded?.sub == "webapi_access_token";
|
||||
|
||||
next();
|
||||
}
|
||||
|
|
37
src/middleware/authenticateAPI.ts
Normal file
37
src/middleware/authenticateAPI.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { Request, Response } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import BadRequestException from "../exceptions/badRequestException";
|
||||
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
|
||||
import InternalException from "../exceptions/internalException";
|
||||
import { JWTHelper } from "../helpers/jwtHelper";
|
||||
|
||||
export default async function authenticateAPI(req: Request, res: Response, next: Function) {
|
||||
const bearer = req.headers.authorization?.split(" ")?.[1] ?? undefined;
|
||||
|
||||
if (!bearer) {
|
||||
throw new BadRequestException("Provide valid Authorization Header");
|
||||
}
|
||||
|
||||
let decoded: string | jwt.JwtPayload;
|
||||
await JWTHelper.validate(bearer)
|
||||
.then((result) => {
|
||||
decoded = result;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err == "jwt expired") {
|
||||
throw new UnauthorizedRequestException("Token expired", err);
|
||||
} else {
|
||||
throw new BadRequestException("Failed Authorization Header decoding", err);
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof decoded == "string" || !decoded) {
|
||||
throw new InternalException("process failed");
|
||||
}
|
||||
|
||||
if (decoded?.sub != "api_token_retrieve") {
|
||||
throw new BadRequestException("This route can only be accessed via a api token");
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
10
src/middleware/preventWebApiAccess.ts
Normal file
10
src/middleware/preventWebApiAccess.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { Request, Response } from "express";
|
||||
import ForbiddenRequestException from "../exceptions/forbiddenRequestException";
|
||||
|
||||
export default async function preventWebapiAccess(req: Request, res: Response, next: Function) {
|
||||
if (req.isWebApiRequest) {
|
||||
throw new ForbiddenRequestException("This route cannot be accessed via webapi");
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
55
src/migrations/1737453096674-addwebapiTokens.ts
Normal file
55
src/migrations/1737453096674-addwebapiTokens.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm";
|
||||
import { DB_TYPE } from "../env.defaults";
|
||||
|
||||
export class AddWebapiTokens1737453096674 implements MigrationInterface {
|
||||
name = "AddWebApiTokens1737453096674";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const variableType_int = DB_TYPE == "mysql" ? "int" : "integer";
|
||||
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: "webapi",
|
||||
columns: [
|
||||
{ name: "id", type: variableType_int, isPrimary: true, isGenerated: true, generationStrategy: "increment" },
|
||||
{ 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: "date", isNullable: true, default: null },
|
||||
],
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: "webapi_permission",
|
||||
columns: [
|
||||
{ name: "webapiId", type: variableType_int, isPrimary: true, isNullable: false },
|
||||
{ name: "permission", type: "varchar", length: "255", isPrimary: true, isNullable: false },
|
||||
],
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
||||
await queryRunner.createForeignKey(
|
||||
"webapi_permission",
|
||||
new TableForeignKey({
|
||||
columnNames: ["webapiId"],
|
||||
referencedColumnNames: ["id"],
|
||||
referencedTableName: "webapi",
|
||||
onDelete: "CASCADE",
|
||||
onUpdate: "RESTRICT",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
const table = await queryRunner.getTable("webapi_permission");
|
||||
const foreignKey = table.foreignKeys.find((fk) => fk.columnNames.indexOf("webapiId") !== -1);
|
||||
await queryRunner.dropForeignKey("webapi_permission", foreignKey);
|
||||
await queryRunner.dropTable("webapi_permission");
|
||||
await queryRunner.dropTable("webapi");
|
||||
}
|
||||
}
|
|
@ -21,6 +21,8 @@ import newsletter from "./club/newsletter";
|
|||
import role from "./user/role";
|
||||
import user from "./user/user";
|
||||
import invite from "./user/invite";
|
||||
import api from "./user/webapi";
|
||||
import preventWebapiAccess from "../../middleware/preventWebApiAccess";
|
||||
|
||||
var router = express.Router({ mergeParams: true });
|
||||
|
||||
|
@ -60,5 +62,6 @@ router.use("/newsletter", PermissionHelper.passCheckMiddleware("read", "club", "
|
|||
router.use("/role", PermissionHelper.passCheckMiddleware("read", "user", "role"), role);
|
||||
router.use("/user", PermissionHelper.passCheckMiddleware("read", "user", "user"), user);
|
||||
router.use("/invite", PermissionHelper.passCheckMiddleware("read", "user", "user"), invite);
|
||||
router.use("/webapi", preventWebapiAccess, PermissionHelper.passCheckMiddleware("read", "user", "webapi"), api);
|
||||
|
||||
export default router;
|
||||
|
|
64
src/routes/admin/user/webapi.ts
Normal file
64
src/routes/admin/user/webapi.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import express, { Request, Response } from "express";
|
||||
import PermissionHelper from "../../../helpers/permissionHelper";
|
||||
import {
|
||||
createWebapi,
|
||||
deleteWebapi,
|
||||
getAllWebapis,
|
||||
getWebapiById,
|
||||
getWebapiPermissions,
|
||||
getWebapiTokenById,
|
||||
updateWebapi,
|
||||
updateWebapiPermissions,
|
||||
} from "../../../controller/admin/user/webapiController";
|
||||
|
||||
var router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
await getAllWebapis(req, res);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
PermissionHelper.passCheckMiddleware("create", "user", "webapi"),
|
||||
async (req: Request, res: Response) => {
|
||||
await createWebapi(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/:id",
|
||||
PermissionHelper.passCheckMiddleware("update", "user", "webapi"),
|
||||
async (req: Request, res: Response) => {
|
||||
await updateWebapi(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/:id/permissions",
|
||||
PermissionHelper.passCheckMiddleware("admin", "user", "webapi"),
|
||||
async (req: Request, res: Response) => {
|
||||
await updateWebapiPermissions(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:id",
|
||||
PermissionHelper.passCheckMiddleware("delete", "user", "webapi"),
|
||||
async (req: Request, res: Response) => {
|
||||
await deleteWebapi(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
|
@ -14,6 +14,8 @@ import auth from "./auth";
|
|||
import admin from "./admin/index";
|
||||
import user from "./user";
|
||||
import detectPWA from "../middleware/detectPWA";
|
||||
import api from "./webapi";
|
||||
import authenticateAPI from "../middleware/authenticateAPI";
|
||||
|
||||
export default (app: Express) => {
|
||||
app.set("query parser", "extended");
|
||||
|
@ -32,6 +34,7 @@ export default (app: Express) => {
|
|||
app.use("/api/reset", reset);
|
||||
app.use("/api/invite", invite);
|
||||
app.use("/api/auth", auth);
|
||||
app.use("/api/webapi", authenticateAPI, api);
|
||||
app.use(authenticate);
|
||||
app.use("/api/admin", admin);
|
||||
app.use("/api/user", user);
|
||||
|
|
10
src/routes/webapi.ts
Normal file
10
src/routes/webapi.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import express, { Request, Response } from "express";
|
||||
import { getWebApiAccess } from "../controller/webapiController";
|
||||
|
||||
var router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get("/retrieve", async (req: Request, res: Response) => {
|
||||
await getWebApiAccess(req, res);
|
||||
});
|
||||
|
||||
export default router;
|
24
src/service/user/webapiPermissionService.ts
Normal file
24
src/service/user/webapiPermissionService.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { dataSource } from "../../data-source";
|
||||
import { webapiPermission } from "../../entity/user/webapi_permission";
|
||||
import InternalException from "../../exceptions/internalException";
|
||||
|
||||
export default abstract class WebapiPermissionService {
|
||||
/**
|
||||
* @description get permission by api
|
||||
* @param webapiId number
|
||||
* @returns {Promise<Array<webapiPermission>>}
|
||||
*/
|
||||
static async getByApi(webapiId: number): Promise<Array<webapiPermission>> {
|
||||
return await dataSource
|
||||
.getRepository(webapiPermission)
|
||||
.createQueryBuilder("webapi_permission")
|
||||
.where("webapi_permission.webapiId = :webapiId", { webapiId: webapiId })
|
||||
.getMany()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException("webapi permissions not found by api", err);
|
||||
});
|
||||
}
|
||||
}
|
83
src/service/user/webapiService.ts
Normal file
83
src/service/user/webapiService.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { dataSource } from "../../data-source";
|
||||
import { webapi } from "../../entity/user/webapi";
|
||||
import InternalException from "../../exceptions/internalException";
|
||||
|
||||
export default abstract class WebapiService {
|
||||
/**
|
||||
* @description get apis
|
||||
* @returns {Promise<Array<webapi>>}
|
||||
*/
|
||||
static async getAll(): Promise<Array<webapi>> {
|
||||
return await dataSource
|
||||
.getRepository(webapi)
|
||||
.createQueryBuilder("webapi")
|
||||
.leftJoinAndSelect("webapi.permissions", "permissions")
|
||||
.getMany()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException("webapis not found", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get api by id
|
||||
* @param id number
|
||||
* @returns {Promise<webapi>}
|
||||
*/
|
||||
static async getById(id: number): Promise<webapi> {
|
||||
return await dataSource
|
||||
.getRepository(webapi)
|
||||
.createQueryBuilder("webapi")
|
||||
.leftJoinAndSelect("webapi.permissions", "permissions")
|
||||
.where("webapi.id = :id", { id: id })
|
||||
.getOneOrFail()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException("webapi not found by id", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get api by token
|
||||
* @param token string
|
||||
* @returns {Promise<webapi>}
|
||||
*/
|
||||
static async getByToken(token: string): Promise<webapi> {
|
||||
return await dataSource
|
||||
.getRepository(webapi)
|
||||
.createQueryBuilder("webapi")
|
||||
.leftJoinAndSelect("webapi.permissions", "permissions")
|
||||
.where("webapi.token = :token", { token: token })
|
||||
.getOneOrFail()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException("webapi not found by token", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get api by id
|
||||
* @param id number
|
||||
* @returns {Promise<webapi>}
|
||||
*/
|
||||
static async getTokenById(id: number): Promise<webapi> {
|
||||
return await dataSource
|
||||
.getRepository(webapi)
|
||||
.createQueryBuilder("webapi")
|
||||
.select("webapi.token")
|
||||
.where("webapi.id = :id", { id: id })
|
||||
.getOneOrFail()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException("webapi token not found by id", err);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ export type PermissionModule =
|
|||
| "calendar_type"
|
||||
| "user"
|
||||
| "role"
|
||||
| "webapi"
|
||||
| "query"
|
||||
| "query_store"
|
||||
| "template"
|
||||
|
@ -55,6 +56,7 @@ export const permissionModules: Array<PermissionModule> = [
|
|||
"calendar_type",
|
||||
"user",
|
||||
"role",
|
||||
"webapi",
|
||||
"query",
|
||||
"query_store",
|
||||
"template",
|
||||
|
@ -75,5 +77,5 @@ export const sectionsAndModules: SectionsAndModulesObject = {
|
|||
"template_usage",
|
||||
"newsletter_config",
|
||||
],
|
||||
user: ["user", "role"],
|
||||
user: ["user", "role", "webapi"],
|
||||
};
|
||||
|
|
10
src/viewmodel/admin/user/webapi.models.ts
Normal file
10
src/viewmodel/admin/user/webapi.models.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { PermissionObject } from "../../../type/permissionTypes";
|
||||
|
||||
export interface ApiViewModel {
|
||||
id: number;
|
||||
permissions: PermissionObject;
|
||||
title: string;
|
||||
createdAt: Date;
|
||||
lastUsage?: Date;
|
||||
expiry?: Date;
|
||||
}
|
Loading…
Reference in a new issue