diff --git a/.env.example b/.env.example index 28b487d..c3fd1f3 100644 --- a/.env.example +++ b/.env.example @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index 3dcc570..17ee5d8 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ services: - MAIL_HOST= - MAIL_PORT= # default ist auf 578 gesetzt - MAIL_SECURE= # default ist auf false gesetzt - - CLUB_NAME= + - CLUB_NAME= # default ist auf FF Admin gesetzt - CLUB_WEBSITE= volumes: - :/app/files diff --git a/package-lock.json b/package-lock.json index e4876a2..0166ff1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "puppeteer": "^23.11.1", "qrcode": "^1.5.4", "reflect-metadata": "^0.2.2", + "rss-parser": "^3.13.0", "socket.io": "^4.7.5", "speakeasy": "^2.0.0", "typeorm": "^0.3.20", @@ -1496,6 +1497,15 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -3152,6 +3162,16 @@ "node": ">= 0.10" } }, + "node_modules/rss-parser": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/rss-parser/-/rss-parser-3.13.0.tgz", + "integrity": "sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==", + "license": "MIT", + "dependencies": { + "entities": "^2.0.3", + "xml2js": "^0.5.0" + } + }, "node_modules/runes2": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/runes2/-/runes2-1.1.4.tgz", @@ -3182,6 +3202,12 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -4253,6 +4279,28 @@ } } }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", diff --git a/package.json b/package.json index 133990d..3c13f5a 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "puppeteer": "^23.11.1", "qrcode": "^1.5.4", "reflect-metadata": "^0.2.2", + "rss-parser": "^3.13.0", "socket.io": "^4.7.5", "speakeasy": "^2.0.0", "typeorm": "^0.3.20", 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 new file mode 100644 index 0000000..4415747 --- /dev/null +++ b/src/command/user/webapi/webapiCommand.ts @@ -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; +} diff --git a/src/command/user/webapi/webapiCommandHandler.ts b/src/command/user/webapi/webapiCommandHandler.ts new file mode 100644 index 0000000..b49f7a6 --- /dev/null +++ b/src/command/user/webapi/webapiCommandHandler.ts @@ -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} + */ + static async create(createWebapi: CreateWebapiCommand): Promise { + 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} + */ + static async update(updateWebapi: UpdateWebapiCommand): Promise { + 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} + */ + 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); + }); + } + + /** + * @description delete api + * @param {DeleteWebapiCommand} deleteWebapi + * @returns {Promise} + */ + static async delete(deleteWebapi: DeleteWebapiCommand): Promise { + return await dataSource + .createQueryBuilder() + .delete() + .from(webapi) + .where("id = :id", { id: deleteWebapi.id }) + .execute() + .then(() => {}) + .catch((err) => { + throw new InternalException("Failed deleting api", err); + }); + } +} diff --git a/src/command/user/webapi/webapiPermissionCommand.ts b/src/command/user/webapi/webapiPermissionCommand.ts new file mode 100644 index 0000000..3942115 --- /dev/null +++ b/src/command/user/webapi/webapiPermissionCommand.ts @@ -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; +} diff --git a/src/command/user/webapi/webapiPermissionCommandHandler.ts b/src/command/user/webapi/webapiPermissionCommandHandler.ts new file mode 100644 index 0000000..74ea514 --- /dev/null +++ b/src/command/user/webapi/webapiPermissionCommandHandler.ts @@ -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} + */ + static async updatePermissions(updateWebapiPermissions: UpdateWebapiPermissionsCommand): Promise { + 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 + ): Promise { + 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 + ): Promise { + 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} + */ + static async create(createPermission: CreateWebapiPermissionCommand): Promise { + 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} + */ + static async delete(deletePermission: DeleteWebapiPermissionCommand): Promise { + 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); + }); + } +} diff --git a/src/controller/admin/club/memberController.ts b/src/controller/admin/club/memberController.ts index d4a9f7e..4970ead 100644 --- a/src/controller/admin/club/memberController.ts +++ b/src/controller/admin/club/memberController.ts @@ -90,6 +90,19 @@ export async function getMemberById(req: Request, res: Response): Promise { res.json(MemberFactory.mapToSingle(member)); } +/** + * @description get member statistics by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getMemberStatisticsById(req: Request, res: Response): Promise { + const memberId = parseInt(req.params.id); + let member = await MemberService.getStatisticsById(memberId); + + res.json(MemberFactory.mapToMemberStatistic(member)); +} + /** * @description get memberships by member * @param req {Request} Express req object @@ -103,6 +116,19 @@ export async function getMembershipsByMember(req: Request, res: Response): Promi res.json(MembershipFactory.mapToBase(memberships)); } +/** + * @description get member statistics by id + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function getMembershipStatisticsById(req: Request, res: Response): Promise { + const memberId = parseInt(req.params.memberId); + let member = await MembershipService.getStatisticsById(memberId); + + res.json(MembershipFactory.mapToBaseStatistics(member)); +} + /** * @description get membership by member and record * @param req {Request} Express req object diff --git a/src/controller/admin/user/webapiController.ts b/src/controller/admin/user/webapiController.ts new file mode 100644 index 0000000..d5390ff --- /dev/null +++ b/src/controller/admin/user/webapiController.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + const id = parseInt(req.params.id); + + let deleteApi: DeleteWebapiCommand = { + id: id, + }; + await WebapiCommandHandler.delete(deleteApi); + + res.sendStatus(204); +} diff --git a/src/controller/webapiController.ts b/src/controller/webapiController.ts new file mode 100644 index 0000000..7af86dd --- /dev/null +++ b/src/controller/webapiController.ts @@ -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 { + 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, + }); +} diff --git a/src/data-source.ts b/src/data-source.ts index c7ba457..8ba9056 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -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", diff --git a/src/entity/user/webapi.ts b/src/entity/user/webapi.ts new file mode 100644 index 0000000..220db8c --- /dev/null +++ b/src/entity/user/webapi.ts @@ -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[]; +} diff --git a/src/entity/user/webapi_permission.ts b/src/entity/user/webapi_permission.ts new file mode 100644 index 0000000..988c7b9 --- /dev/null +++ b/src/entity/user/webapi_permission.ts @@ -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; +} diff --git a/src/env.defaults.ts b/src/env.defaults.ts index 33fe53a..d4e3c3b 100644 --- a/src/env.defaults.ts +++ b/src/env.defaults.ts @@ -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() { diff --git a/src/factory/admin/club/member/member.ts b/src/factory/admin/club/member/member.ts index 42faf7c..116a0c8 100644 --- a/src/factory/admin/club/member/member.ts +++ b/src/factory/admin/club/member/member.ts @@ -1,5 +1,6 @@ import { member } from "../../../../entity/club/member/member"; -import { MemberViewModel } from "../../../../viewmodel/admin/club/member/member.models"; +import { MemberStatisticsViewModel, MemberViewModel } from "../../../../viewmodel/admin/club/member/member.models"; +import { memberView } from "../../../../views/memberView"; import CommunicationFactory from "./communication"; import MembershipFactory from "./membership"; @@ -40,4 +41,23 @@ export default abstract class MemberFactory { public static mapToBase(records: Array): Array { return records.map((r) => this.mapToSingle(r)); } + + /** + * @description map view record to MemberMembershipStatisticsViewModel + * @param {memberView} record + * @returns {MemberStatisticsViewModel} + */ + public static mapToMemberStatistic(record: memberView): MemberStatisticsViewModel { + return { + id: record.id, + salutation: record.salutation, + firstname: record.firstname, + lastname: record.lastname, + nameaffix: record.nameaffix, + birthdate: record.birthdate, + todayAge: record.todayAge, + ageThisYear: record.ageThisYear, + exactAge: record.exactAge, + }; + } } diff --git a/src/factory/admin/club/member/membership.ts b/src/factory/admin/club/member/membership.ts index 8736785..42980ab 100644 --- a/src/factory/admin/club/member/membership.ts +++ b/src/factory/admin/club/member/membership.ts @@ -1,5 +1,9 @@ import { membership } from "../../../../entity/club/member/membership"; -import { MembershipViewModel } from "../../../../viewmodel/admin/club/member/membership.models"; +import { + MembershipStatisticsViewModel, + MembershipViewModel, +} from "../../../../viewmodel/admin/club/member/membership.models"; +import { membershipView } from "../../../../views/membershipsView"; export default abstract class MembershipFactory { /** @@ -26,4 +30,33 @@ export default abstract class MembershipFactory { public static mapToBase(records: Array): Array { return records.map((r) => this.mapToSingle(r)); } + + /** + * @description map view record to MembershipStatisticsViewModel + * @param {membershipView} record + * @returns {MembershipStatisticsViewModel} + */ + public static mapToSingleStatistic(record: membershipView): MembershipStatisticsViewModel { + return { + durationInDays: record.durationInDays, + durationInYears: record.durationInYears, + status: record.status, + statusId: record.statusId, + memberId: record.memberId, + memberSalutation: record.memberSalutation, + memberFirstname: record.memberFirstname, + memberLastname: record.memberLastname, + memberNameaffix: record.memberNameaffix, + memberBirthdate: record.memberBirthdate, + }; + } + + /** + * @description map records to MembershipStatisticsViewModel + * @param {Array} records + * @returns {Array} + */ + public static mapToBaseStatistics(records: Array): Array { + return records.map((r) => this.mapToSingleStatistic(r)); + } } diff --git a/src/factory/admin/user/webapi.ts b/src/factory/admin/user/webapi.ts new file mode 100644 index 0000000..3c0ede4 --- /dev/null +++ b/src/factory/admin/user/webapi.ts @@ -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} records + * @returns {Array} + */ + public static mapToBase(records: Array): Array { + return records.map((r) => this.mapToSingle(r)); + } +} diff --git a/src/helpers/jwtHelper.ts b/src/helpers/jwtHelper.ts index b8a62a5..791af2a 100644 --- a/src/helpers/jwtHelper.ts +++ b/src/helpers/jwtHelper.ts @@ -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 { @@ -17,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); @@ -72,4 +78,35 @@ export abstract class JWTHelper { throw new InternalException("Failed accessToken creation", err); }); } + + 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); + 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); + }); + } } diff --git a/src/helpers/permissionHelper.ts b/src/helpers/permissionHelper.ts index 63edffa..990e97d 100644 --- a/src/helpers/permissionHelper.ts +++ b/src/helpers/permissionHelper.ts @@ -89,6 +89,19 @@ export default class PermissionHelper { }; } + static isAdminMiddleware(): (req: Request, res: Response, next: Function) => void { + return (req: Request, res: Response, next: Function) => { + const permissions = req.permissions; + const isOwner = req.isOwner; + + if (isOwner || permissions.admin) { + next(); + } else { + throw new ForbiddenRequestException(`missing admin permission`); + } + }; + } + static convertToObject(permissions: Array): PermissionObject { if (permissions.includes("*")) { return { diff --git a/src/index.ts b/src/index.ts index f5a73e6..68eef4f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ declare global { isOwner: boolean; permissions: PermissionObject; isPWA: boolean; + isWebApiRequest: boolean; } } } diff --git a/src/middleware/authenticate.ts b/src/middleware/authenticate.ts index cfa1f56..9e62bb4 100644 --- a/src/middleware/authenticate.ts +++ b/src/middleware/authenticate.ts @@ -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(); } diff --git a/src/middleware/authenticateAPI.ts b/src/middleware/authenticateAPI.ts new file mode 100644 index 0000000..b05060e --- /dev/null +++ b/src/middleware/authenticateAPI.ts @@ -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(); +} diff --git a/src/middleware/preventWebApiAccess.ts b/src/middleware/preventWebApiAccess.ts new file mode 100644 index 0000000..3c3b7c6 --- /dev/null +++ b/src/middleware/preventWebApiAccess.ts @@ -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(); + } +} diff --git a/src/migrations/1737453096674-addwebapiTokens.ts b/src/migrations/1737453096674-addwebapiTokens.ts new file mode 100644 index 0000000..55f37fb --- /dev/null +++ b/src/migrations/1737453096674-addwebapiTokens.ts @@ -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 { + 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 { + 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"); + } +} diff --git a/src/routes/admin/club/member.ts b/src/routes/admin/club/member.ts index f014d37..66e57aa 100644 --- a/src/routes/admin/club/member.ts +++ b/src/routes/admin/club/member.ts @@ -1,36 +1,38 @@ import express, { Request, Response } from "express"; import { - addAwardToMember, - addCommunicationToMember, - addExecutivePositionToMember, - addMembershipToMember, - addQualificationToMember, - createMember, - createMemberPrintoutList, - deleteAwardOfMember, - deleteCommunicationOfMember, - deleteExecutivePositionOfMember, - deleteMemberById, - deleteMembershipOfMember, - deleteQualificationOfMember, - getAllMembers, - getAwardByMemberAndRecord, - getAwardsByMember, - getCommunicationByMemberAndRecord, - getCommunicationsByMember, - getExecutivePositionByMemberAndRecord, - getExecutivePositionsByMember, - getMemberById, - getMembershipByMemberAndRecord, - getMembershipsByMember, - getQualificationByMemberAndRecord, - getQualificationsByMember, - updateAwardOfMember, - updateCommunicationOfMember, - updateExecutivePositionOfMember, - updateMemberById, - updateMembershipOfMember, - updateQualificationOfMember, + addAwardToMember, + addCommunicationToMember, + addExecutivePositionToMember, + addMembershipToMember, + addQualificationToMember, + createMember, + createMemberPrintoutList, + deleteAwardOfMember, + deleteCommunicationOfMember, + deleteExecutivePositionOfMember, + deleteMemberById, + deleteMembershipOfMember, + deleteQualificationOfMember, + getAllMembers, + getAwardByMemberAndRecord, + getAwardsByMember, + getCommunicationByMemberAndRecord, + getCommunicationsByMember, + getExecutivePositionByMemberAndRecord, + getExecutivePositionsByMember, + getMemberById, + getMembershipByMemberAndRecord, + getMembershipsByMember, + getMembershipStatisticsById, + getMemberStatisticsById, + getQualificationByMemberAndRecord, + getQualificationsByMember, + updateAwardOfMember, + updateCommunicationOfMember, + updateExecutivePositionOfMember, + updateMemberById, + updateMembershipOfMember, + updateQualificationOfMember, } from "../../../controller/admin/club/memberController"; import PermissionHelper from "../../../helpers/permissionHelper"; @@ -44,14 +46,22 @@ router.get("/:id", async (req: Request, res: Response) => { await getMemberById(req, res); }); +router.get("/:id/statistics", async (req: Request, res: Response) => { + await getMemberStatisticsById(req, res); +}); + router.get("/print/namelist", async (req: Request, res: Response) => { - await createMemberPrintoutList(req, res); + await createMemberPrintoutList(req, res); }); router.get("/:memberId/memberships", async (req: Request, res: Response) => { await getMembershipsByMember(req, res); }); +router.get("/:memberId/memberships/statistics", async (req: Request, res: Response) => { + await getMembershipStatisticsById(req, res); +}); + router.get("/:memberId/membership/:id", async (req: Request, res: Response) => { await getMembershipByMemberAndRecord(req, res); }); diff --git a/src/routes/admin/index.ts b/src/routes/admin/index.ts index cd7c935..9aa24ce 100644 --- a/src/routes/admin/index.ts +++ b/src/routes/admin/index.ts @@ -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; diff --git a/src/routes/admin/user/webapi.ts b/src/routes/admin/user/webapi.ts new file mode 100644 index 0000000..0d0e7ab --- /dev/null +++ b/src/routes/admin/user/webapi.ts @@ -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; diff --git a/src/routes/index.ts b/src/routes/index.ts index b939d52..ed10173 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -14,6 +14,10 @@ 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"; +import server from "./server"; +import PermissionHelper from "../helpers/permissionHelper"; export default (app: Express) => { app.set("query parser", "extended"); @@ -32,8 +36,10 @@ 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); + app.use("/api/server", PermissionHelper.isAdminMiddleware(), server); app.use(errorHandler); }; diff --git a/src/routes/server.ts b/src/routes/server.ts new file mode 100644 index 0000000..964d207 --- /dev/null +++ b/src/routes/server.ts @@ -0,0 +1,35 @@ +import express, { Request, Response } from "express"; +import { FileSystemHelper } from "../helpers/fileSystemHelper"; +import Parser from "rss-parser"; + +var router = express.Router({ mergeParams: true }); + +router.get("/version", async (req: Request, res: Response) => { + let serverPackage = FileSystemHelper.readTemplateFile("/package.json"); + let serverJson = JSON.parse(serverPackage); + res.send({ + name: serverJson.name, + description: serverJson.description, + version: serverJson.version, + author: serverJson.author, + license: serverJson.license, + }); +}); + +router.get("/settings", async (req: Request, res: Response) => { + res.json({}); +}); + +router.get("/serverrss", async (req: Request, res: Response) => { + const parser = new Parser(); + let feed = await parser.parseURL("https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin-server/releases.rss"); + res.json(feed); +}); + +router.get("/clientrss", async (req: Request, res: Response) => { + const parser = new Parser(); + let feed = await parser.parseURL("https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin/releases.rss"); + res.json(feed); +}); + +export default router; diff --git a/src/routes/webapi.ts b/src/routes/webapi.ts new file mode 100644 index 0000000..164802c --- /dev/null +++ b/src/routes/webapi.ts @@ -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; diff --git a/src/service/club/member/memberService.ts b/src/service/club/member/memberService.ts index c7da55e..3b08f9f 100644 --- a/src/service/club/member/memberService.ts +++ b/src/service/club/member/memberService.ts @@ -2,6 +2,7 @@ import { dataSource } from "../../../data-source"; import { member } from "../../../entity/club/member/member"; import { membership } from "../../../entity/club/member/membership"; import InternalException from "../../../exceptions/internalException"; +import { memberView } from "../../../views/memberView"; export default abstract class MemberService { /** @@ -132,6 +133,25 @@ export default abstract class MemberService { }); } + /** + * @description get member statistics by id + * @param {number} id + * @returns {Promise} + */ + static async getStatisticsById(id: number): Promise { + return await dataSource + .getRepository(memberView) + .createQueryBuilder("memberView") + .where("memberView.id = :id", { id: id }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("memberView not found by id", err); + }); + } + /** * @description get members where membership is setz * @returns {Promise} diff --git a/src/service/club/member/membershipService.ts b/src/service/club/member/membershipService.ts index a00daf2..8ef99e1 100644 --- a/src/service/club/member/membershipService.ts +++ b/src/service/club/member/membershipService.ts @@ -1,6 +1,7 @@ import { dataSource } from "../../../data-source"; import { membership } from "../../../entity/club/member/membership"; import InternalException from "../../../exceptions/internalException"; +import { membershipView } from "../../../views/membershipsView"; export default abstract class MembershipService { /** @@ -45,4 +46,23 @@ export default abstract class MembershipService { throw new InternalException("member membership not found by id", err); }); } + + /** + * @description get membership statistics by memberId + * @param {number} memberId + * @returns {Promise>} + */ + static async getStatisticsById(memberId: number): Promise> { + return await dataSource + .getRepository(membershipView) + .createQueryBuilder("membershipView") + .where("membershipView.memberId = :memberId", { memberId: memberId }) + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("membershipView not found by id", err); + }); + } } diff --git a/src/service/user/webapiPermissionService.ts b/src/service/user/webapiPermissionService.ts new file mode 100644 index 0000000..a210353 --- /dev/null +++ b/src/service/user/webapiPermissionService.ts @@ -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>} + */ + static async getByApi(webapiId: number): Promise> { + 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); + }); + } +} diff --git a/src/service/user/webapiService.ts b/src/service/user/webapiService.ts new file mode 100644 index 0000000..0faa51a --- /dev/null +++ b/src/service/user/webapiService.ts @@ -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>} + */ + static async getAll(): Promise> { + 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} + */ + static async getById(id: number): Promise { + 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} + */ + static async getByToken(token: string): Promise { + 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} + */ + static async getTokenById(id: number): Promise { + 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); + }); + } +} diff --git a/src/type/permissionTypes.ts b/src/type/permissionTypes.ts index 95ea543..5ba5cb4 100644 --- a/src/type/permissionTypes.ts +++ b/src/type/permissionTypes.ts @@ -14,6 +14,7 @@ export type PermissionModule = | "calendar_type" | "user" | "role" + | "webapi" | "query" | "query_store" | "template" @@ -55,6 +56,7 @@ export const permissionModules: Array = [ "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"], }; diff --git a/src/viewmodel/admin/club/member/member.models.ts b/src/viewmodel/admin/club/member/member.models.ts index 60e1a27..c28ed73 100644 --- a/src/viewmodel/admin/club/member/member.models.ts +++ b/src/viewmodel/admin/club/member/member.models.ts @@ -16,3 +16,15 @@ export interface MemberViewModel { smsAlarming?: Array; preferredCommunication?: Array; } + +export interface MemberStatisticsViewModel { + id: number; + salutation: Salutation; + firstname: string; + lastname: string; + nameaffix: string; + birthdate: Date; + todayAge: number; + ageThisYear: number; + exactAge: string; +} diff --git a/src/viewmodel/admin/club/member/membership.models.ts b/src/viewmodel/admin/club/member/membership.models.ts index 59e2aba..da8bffa 100644 --- a/src/viewmodel/admin/club/member/membership.models.ts +++ b/src/viewmodel/admin/club/member/membership.models.ts @@ -1,3 +1,5 @@ +import { Salutation } from "../../../../enums/salutation"; + export interface MembershipViewModel { id: number; start: Date; @@ -6,3 +8,16 @@ export interface MembershipViewModel { status: string; statusId: number; } + +export interface MembershipStatisticsViewModel { + durationInDays: number; + durationInYears: string; + status: string; + statusId: number; + memberId: number; + memberSalutation: Salutation; + memberFirstname: string; + memberLastname: string; + memberNameaffix: string; + memberBirthdate: Date; +} diff --git a/src/viewmodel/admin/user/webapi.models.ts b/src/viewmodel/admin/user/webapi.models.ts new file mode 100644 index 0000000..cf3a861 --- /dev/null +++ b/src/viewmodel/admin/user/webapi.models.ts @@ -0,0 +1,10 @@ +import { PermissionObject } from "../../../type/permissionTypes"; + +export interface ApiViewModel { + id: number; + permissions: PermissionObject; + title: string; + createdAt: Date; + lastUsage?: Date; + expiry?: Date; +} diff --git a/src/views/memberView.ts b/src/views/memberView.ts index 1743ed4..6a980f0 100644 --- a/src/views/memberView.ts +++ b/src/views/memberView.ts @@ -43,5 +43,5 @@ export class memberView { ageThisYear: number; @ViewColumn() - exactAge: Date; + exactAge: string; } diff --git a/src/views/membershipsView.ts b/src/views/membershipsView.ts index 0b5df92..1e40209 100644 --- a/src/views/membershipsView.ts +++ b/src/views/membershipsView.ts @@ -30,7 +30,7 @@ export class membershipView { durationInDays: number; @ViewColumn() - durationInYears: Date; + durationInYears: string; @ViewColumn() status: string;