diff --git a/src/command/userCommand.ts b/src/command/userCommand.ts index a084115..634a993 100644 --- a/src/command/userCommand.ts +++ b/src/command/userCommand.ts @@ -4,6 +4,7 @@ export interface CreateUserCommand { firstname: string; lastname: string; secret: string; + isOwner: boolean; } export interface UpdateUserCommand { diff --git a/src/command/userCommandHandler.ts b/src/command/userCommandHandler.ts index 79834e2..876db24 100644 --- a/src/command/userCommandHandler.ts +++ b/src/command/userCommandHandler.ts @@ -22,6 +22,7 @@ export default abstract class UserCommandHandler { firstname: createUser.firstname, lastname: createUser.lastname, secret: createUser.secret, + isOwner: createUser.isOwner, }) .execute() .then((result) => { diff --git a/src/controller/authController.ts b/src/controller/authController.ts index cbd1a1d..f68aab2 100644 --- a/src/controller/authController.ts +++ b/src/controller/authController.ts @@ -22,7 +22,7 @@ export async function login(req: Request, res: Response): Promise { let username = req.body.username; let totp = req.body.totp; - let { id, secret, mail, firstname, lastname } = await UserService.getByUsername(username); + let { id, secret, mail, firstname, lastname, isOwner } = await UserService.getByUsername(username); let valid = speakeasy.totp.verify({ secret: secret, @@ -48,6 +48,7 @@ export async function login(req: Request, res: Response): Promise { username: username, firstname: firstname, lastname: lastname, + isOwner: isOwner, permissions: permissionObject, }; @@ -105,7 +106,7 @@ export async function refresh(req: Request, res: Response): Promise { throw new UnauthorizedRequestException("user not identified with token and refresh"); } - let { id, username, mail, firstname, lastname } = await UserService.getById(tokenUserId); + let { id, username, mail, firstname, lastname, isOwner } = await UserService.getById(tokenUserId); let permissions = await UserPermissionService.getByUser(id); let permissionStrings = permissions.map((e) => e.permission); @@ -117,6 +118,7 @@ export async function refresh(req: Request, res: Response): Promise { username: username, firstname: firstname, lastname: lastname, + isOwner: isOwner, permissions: permissionObject, }; diff --git a/src/controller/inviteController.ts b/src/controller/inviteController.ts index f9be5b3..0b2b207 100644 --- a/src/controller/inviteController.ts +++ b/src/controller/inviteController.ts @@ -124,23 +124,17 @@ export async function finishInvite(req: Request, res: Response, grantAdmin: bool lastname: lastname, mail: mail, secret: secret, + isOwner: grantAdmin, }; let id = await UserCommandHandler.create(createUser); - if (grantAdmin) { - let createPermission: CreateUserPermissionCommand = { - permission: "*", - userId: id, - }; - await UserPermissionCommandHandler.create(createPermission); - } - let jwtData: JWTToken = { userId: id, mail: mail, username: username, firstname: firstname, lastname: lastname, + isOwner: grantAdmin, permissions: { ...(grantAdmin ? { admin: true } : {}), }, diff --git a/src/data-source.ts b/src/data-source.ts index a78ab3c..2701efb 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -29,6 +29,7 @@ import { memberQualifications } from "./entity/memberQualifications"; import { membership } from "./entity/membership"; import { Memberdata1726301836849 } from "./migrations/1726301836849-memberdata"; import { CommunicationFields1727439800630 } from "./migrations/1727439800630-communicationFields"; +import { Ownership1728313041449 } from "./migrations/1728313041449-ownership"; const dataSource = new DataSource({ type: DB_TYPE as any, @@ -68,6 +69,7 @@ const dataSource = new DataSource({ MemberBaseData1725435669492, Memberdata1726301836849, CommunicationFields1727439800630, + Ownership1728313041449, ], migrationsRun: true, migrationsTransactionMode: "each", diff --git a/src/entity/user.ts b/src/entity/user.ts index fcb31c6..09435cd 100644 --- a/src/entity/user.ts +++ b/src/entity/user.ts @@ -22,6 +22,9 @@ export class user { @Column({ type: "varchar", length: 255 }) secret: string; + @Column({ type: "boolean", default: false }) + isOwner: boolean; + @ManyToMany(() => role, (role) => role.users, { nullable: false, onDelete: "CASCADE", diff --git a/src/factory/admin/user.ts b/src/factory/admin/user.ts index 2f7098c..ca8524f 100644 --- a/src/factory/admin/user.ts +++ b/src/factory/admin/user.ts @@ -21,6 +21,7 @@ export default abstract class UserFactory { firstname: record.firstname, lastname: record.lastname, mail: record.mail, + isOwner: record.isOwner, permissions: PermissionHelper.convertToObject(userPermissionStrings), roles: RoleFactory.mapToBase(record.roles), permissions_total: totalPermissions, diff --git a/src/helpers/permissionHelper.ts b/src/helpers/permissionHelper.ts index 2eb4def..63edffa 100644 --- a/src/helpers/permissionHelper.ts +++ b/src/helpers/permissionHelper.ts @@ -55,8 +55,9 @@ export default class PermissionHelper { ): (req: Request, res: Response, next: Function) => void { return (req: Request, res: Response, next: Function) => { const permissions = req.permissions; + const isOwner = req.isOwner; - if (this.can(permissions, requiredPermissions, section, module)) { + if (isOwner || this.can(permissions, requiredPermissions, section, module)) { next(); } else { throw new ForbiddenRequestException( @@ -74,8 +75,9 @@ export default class PermissionHelper { ): (req: Request, res: Response, next: Function) => void { return (req: Request, res: Response, next: Function) => { const permissions = req.permissions; + const isOwner = req.isOwner; - if (this.canSection(permissions, requiredPermissions, section)) { + if (isOwner || this.canSection(permissions, requiredPermissions, section)) { next(); } else { throw new ForbiddenRequestException( diff --git a/src/index.ts b/src/index.ts index d551a1e..6fd0f99 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ declare global { export interface Request { userId: string; username: string; + isOwner: boolean; permissions: PermissionObject; } } diff --git a/src/middleware/authenticate.ts b/src/middleware/authenticate.ts index a62c689..cfa1f56 100644 --- a/src/middleware/authenticate.ts +++ b/src/middleware/authenticate.ts @@ -31,6 +31,7 @@ export default async function authenticate(req: Request, res: Response, next: Fu req.userId = decoded.userId; req.username = decoded.username; + req.isOwner = decoded.isOwner; req.permissions = decoded.permissions; next(); diff --git a/src/migrations/1728313041449-ownership.ts b/src/migrations/1728313041449-ownership.ts new file mode 100644 index 0000000..bf744a2 --- /dev/null +++ b/src/migrations/1728313041449-ownership.ts @@ -0,0 +1,56 @@ +import { MigrationInterface, QueryRunner, TableColumn } from "typeorm"; + +export class Ownership1728313041449 implements MigrationInterface { + name = "Ownership1728313041449"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn( + "user", + new TableColumn({ + name: "isOwner", + type: "tinyint", + default: 0, + isNullable: false, + }) + ); + + await queryRunner.manager + .createQueryBuilder() + .update("user") + .set({ isOwner: 1 }) + .where((qb) => { + const subQuery = queryRunner.manager + .createQueryBuilder() + .select("1") + .from("user_permission", "up") + .where("user.id = up.userId") + .andWhere("up.permission = '*'") + .getQuery(); + return `EXISTS (${subQuery})`; + }) + .execute(); + + await queryRunner.manager.createQueryBuilder().delete().from("user_permission").where("permission = '*'").execute(); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.manager + .createQueryBuilder() + .insert() + .into("user_permission") + .values( + await queryRunner.manager + .createQueryBuilder() + .select("user.id", "userId") + .addSelect("'*'", "permission") + .from("user", "user") + .where("user.isOwner = 1") + .execute() + ) + .execute(); + + await queryRunner.manager.createQueryBuilder().update("user").set({ isOwner: 0 }).where("isOwner = 1").execute(); + + await queryRunner.dropColumn("user", "isOwner"); + } +} diff --git a/src/type/jwtTypes.ts b/src/type/jwtTypes.ts index 83e9a44..1d65d38 100644 --- a/src/type/jwtTypes.ts +++ b/src/type/jwtTypes.ts @@ -1,7 +1,7 @@ import { PermissionObject } from "./permissionTypes"; export type JWTData = { - [key: string]: string | number | PermissionObject; + [key: string]: string | number | boolean | PermissionObject; }; export type JWTToken = { @@ -10,6 +10,7 @@ export type JWTToken = { username: string; firstname: string; lastname: string; + isOwner: boolean; permissions: PermissionObject; } & JWTData; diff --git a/src/viewmodel/admin/user.models.ts b/src/viewmodel/admin/user.models.ts index 39ee152..df6eaa3 100644 --- a/src/viewmodel/admin/user.models.ts +++ b/src/viewmodel/admin/user.models.ts @@ -7,6 +7,7 @@ export interface UserViewModel { mail: string; firstname: string; lastname: string; + isOwner: boolean; permissions: PermissionObject; roles: Array; permissions_total: PermissionObject;