permission system - permission formatting
This commit is contained in:
parent
d889f92643
commit
2f5d9d3f01
15 changed files with 352 additions and 18 deletions
10
src/command/permissionCommand.ts
Normal file
10
src/command/permissionCommand.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { PermissionString } from "../type/permissionTypes";
|
||||
|
||||
export interface CreatePermissionCommand {
|
||||
permission: PermissionString;
|
||||
userId: number;
|
||||
}
|
||||
|
||||
export interface DeletePermissionCommand {
|
||||
id: number;
|
||||
}
|
48
src/command/permissionCommandHandler.ts
Normal file
48
src/command/permissionCommandHandler.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { dataSource } from "../data-source";
|
||||
import { permission } from "../entity/permission";
|
||||
import InternalException from "../exceptions/internalException";
|
||||
import UserService from "../service/userService";
|
||||
import { CreatePermissionCommand, DeletePermissionCommand } from "./permissionCommand";
|
||||
|
||||
export default abstract class PermissionCommandHandler {
|
||||
/**
|
||||
* @description grant permission to user
|
||||
* @param CreatePermissionCommand
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
static async create(createPermission: CreatePermissionCommand): Promise<number> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(permission)
|
||||
.values({
|
||||
permission: createPermission.permission,
|
||||
user: await UserService.getById(createPermission.userId),
|
||||
})
|
||||
.execute()
|
||||
.then((result) => {
|
||||
return result.identifiers[0].id;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException("Failed saving permission");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description remove permission to user
|
||||
* @param DeletePermissionCommand
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
static async deleteByToken(deletePermission: DeletePermissionCommand): Promise<any> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(permission)
|
||||
.where("permission.id = :id", { id: deletePermission.id })
|
||||
.execute()
|
||||
.then((res) => {})
|
||||
.catch((err) => {
|
||||
throw new InternalException("failed permission removal");
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,17 +1,15 @@
|
|||
import { Request, Response } from "express";
|
||||
import { JWTHelper } from "../helpers/jwtHelper";
|
||||
import { JWTData, JWTToken } from "../type/jwtTypes";
|
||||
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/userService";
|
||||
import speakeasy from "speakeasy";
|
||||
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
|
||||
import QRCode from "qrcode";
|
||||
import { CreateUserCommand } from "../command/userCommand";
|
||||
import UserCommandHandler from "../command/userCommandHandler";
|
||||
import RefreshService from "../service/refreshService";
|
||||
import BadRequestException from "../exceptions/badRequestException";
|
||||
import PermissionService from "../service/permissionService";
|
||||
import PermissionHelper from "../helpers/permissionHelper";
|
||||
|
||||
/**
|
||||
* @description Check authentication status by token
|
||||
|
@ -23,7 +21,7 @@ export async function login(req: Request, res: Response): Promise<any> {
|
|||
let username = req.body.username;
|
||||
let totp = req.body.totp;
|
||||
|
||||
let { id, secret } = await UserService.getByUsername(username);
|
||||
let { id, secret, mail, firstname, lastname } = await UserService.getByUsername(username);
|
||||
|
||||
let valid = speakeasy.totp.verify({
|
||||
secret: secret,
|
||||
|
@ -36,10 +34,17 @@ export async function login(req: Request, res: Response): Promise<any> {
|
|||
throw new UnauthorizedRequestException("Token not valid or expired");
|
||||
}
|
||||
|
||||
let permissions = await PermissionService.getByUser(id);
|
||||
let permissionStrings = permissions.map((e) => e.permission);
|
||||
let permissionObject = PermissionHelper.convertToObject(permissionStrings);
|
||||
|
||||
let jwtData: JWTToken = {
|
||||
userId: id,
|
||||
mail: mail,
|
||||
username: username,
|
||||
rights: [],
|
||||
firstname: firstname,
|
||||
lastname: lastname,
|
||||
permissions: permissionObject,
|
||||
};
|
||||
|
||||
let accessToken: string;
|
||||
|
@ -96,12 +101,19 @@ export async function refresh(req: Request, res: Response): Promise<any> {
|
|||
throw new UnauthorizedRequestException("user not identified with token and refresh");
|
||||
}
|
||||
|
||||
let { id, username } = await UserService.getById(tokenUserId);
|
||||
let { id, username, mail, firstname, lastname } = await UserService.getById(tokenUserId);
|
||||
|
||||
let permissions = await PermissionService.getByUser(id);
|
||||
let permissionStrings = permissions.map((e) => e.permission);
|
||||
let permissionObject = PermissionHelper.convertToObject(permissionStrings);
|
||||
|
||||
let jwtData: JWTToken = {
|
||||
userId: id,
|
||||
mail: mail,
|
||||
username: username,
|
||||
rights: [],
|
||||
firstname: firstname,
|
||||
lastname: lastname,
|
||||
permissions: permissionObject,
|
||||
};
|
||||
|
||||
let accessToken: string;
|
||||
|
|
|
@ -16,6 +16,8 @@ import InviteService from "../service/inviteService";
|
|||
import UserService from "../service/userService";
|
||||
import CustomRequestException from "../exceptions/customRequestException";
|
||||
import { CLUB_NAME } from "../env.defaults";
|
||||
import { CreatePermissionCommand } from "../command/permissionCommand";
|
||||
import PermissionCommandHandler from "../command/permissionCommandHandler";
|
||||
|
||||
/**
|
||||
* @description start first user
|
||||
|
@ -98,7 +100,7 @@ export async function verifyInvite(req: Request, res: Response): Promise<any> {
|
|||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function finishInvite(req: Request, res: Response): Promise<any> {
|
||||
export async function finishInvite(req: Request, res: Response, grantAdmin: boolean = false): Promise<any> {
|
||||
let mail = req.body.mail;
|
||||
let token = req.body.token;
|
||||
let totp = req.body.totp;
|
||||
|
@ -127,10 +129,23 @@ export async function finishInvite(req: Request, res: Response): Promise<any> {
|
|||
};
|
||||
let id = await UserCommandHandler.create(createUser);
|
||||
|
||||
if (grantAdmin) {
|
||||
let createPermission: CreatePermissionCommand = {
|
||||
permission: "*",
|
||||
userId: id,
|
||||
};
|
||||
await PermissionCommandHandler.create(createPermission);
|
||||
}
|
||||
|
||||
let jwtData: JWTToken = {
|
||||
userId: id,
|
||||
mail: mail,
|
||||
username: username,
|
||||
rights: [],
|
||||
firstname: firstname,
|
||||
lastname: lastname,
|
||||
permissions: {
|
||||
...(grantAdmin ? { admin: true } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
let accessToken: string;
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import "dotenv/config";
|
||||
import "reflect-metadata";
|
||||
import { DataSource } from "typeorm";
|
||||
import { DB_HOST, DB_USERNAME, DB_PASSWORD, DB_NAME } from "./env.defaults";
|
||||
|
||||
import { user } from "./entity/user";
|
||||
import { refresh } from "./entity/refresh";
|
||||
import { invite } from "./entity/invite";
|
||||
import { permission } from "./entity/permission";
|
||||
|
||||
import { Initial1724317398939 } from "./migrations/1724317398939-initial";
|
||||
import { RefreshPrimaryChange1724573307851 } from "./migrations/1724573307851-refreshPrimaryChange";
|
||||
import { Invite1724579024939 } from "./migrations/1724579024939-invite";
|
||||
import { DB_HOST, DB_USERNAME, DB_PASSWORD, DB_NAME } from "./env.defaults";
|
||||
import { Permissions1724661484664 } from "./migrations/1724661484664-permissions";
|
||||
|
||||
const dataSource = new DataSource({
|
||||
type: "mysql",
|
||||
|
@ -21,8 +23,8 @@ const dataSource = new DataSource({
|
|||
synchronize: false,
|
||||
logging: process.env.NODE_ENV ? true : ["schema", "error", "warn", "log", "migration"],
|
||||
bigNumberStrings: false,
|
||||
entities: [user, refresh, invite],
|
||||
migrations: [Initial1724317398939, RefreshPrimaryChange1724573307851, Invite1724579024939],
|
||||
entities: [user, refresh, invite, permission],
|
||||
migrations: [Initial1724317398939, RefreshPrimaryChange1724573307851, Invite1724579024939, Permissions1724661484664],
|
||||
migrationsRun: true,
|
||||
migrationsTransactionMode: "each",
|
||||
subscribers: [],
|
||||
|
|
15
src/entity/permission.ts
Normal file
15
src/entity/permission.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm";
|
||||
import { user } from "./user";
|
||||
import { PermissionObject, PermissionString } from "../type/permissionTypes";
|
||||
|
||||
@Entity()
|
||||
export class permission {
|
||||
@PrimaryColumn({ type: "int" })
|
||||
userId: number;
|
||||
|
||||
@PrimaryColumn({ type: "varchar", length: 255 })
|
||||
permission: PermissionString;
|
||||
|
||||
@ManyToOne(() => user)
|
||||
user: user;
|
||||
}
|
7
src/exceptions/forbiddenRequestException.ts
Normal file
7
src/exceptions/forbiddenRequestException.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import CustomRequestException from "./customRequestException";
|
||||
|
||||
export default class ForbiddenRequestException extends CustomRequestException {
|
||||
constructor(msg: string) {
|
||||
super(403, msg);
|
||||
}
|
||||
}
|
124
src/helpers/permissionHelper.ts
Normal file
124
src/helpers/permissionHelper.ts
Normal file
|
@ -0,0 +1,124 @@
|
|||
import { Request, Response } from "express";
|
||||
import {
|
||||
PermissionModule,
|
||||
permissionModules,
|
||||
PermissionObject,
|
||||
PermissionSection,
|
||||
PermissionString,
|
||||
PermissionType,
|
||||
permissionTypes,
|
||||
} from "../type/permissionTypes";
|
||||
import ForbiddenRequestException from "../exceptions/forbiddenRequestException";
|
||||
|
||||
export default class PermissionHelper {
|
||||
static passCheckMiddleware(
|
||||
section: PermissionSection,
|
||||
module: PermissionModule,
|
||||
requiredPermissions: Array<PermissionType> | "*"
|
||||
): (req: Request, res: Response, next: Function) => void {
|
||||
return (req: Request, res: Response, next: Function) => {
|
||||
const permissions = req.rights;
|
||||
|
||||
if (permissions.admin) {
|
||||
next();
|
||||
} else if (permissions?.[section]?.all) {
|
||||
next();
|
||||
} else if (permissions?.[section]?.all) {
|
||||
next();
|
||||
} else if (permissions?.[section]?.[module] == "*") {
|
||||
next();
|
||||
} else if (
|
||||
(permissions?.[section]?.[module] as Array<PermissionType>).some((e: PermissionType) =>
|
||||
requiredPermissions.includes(e)
|
||||
)
|
||||
) {
|
||||
next();
|
||||
} else {
|
||||
throw new ForbiddenRequestException(
|
||||
`missing permission for ${section}.${module}.${
|
||||
Array.isArray(requiredPermissions) ? requiredPermissions.join("|") : requiredPermissions
|
||||
}`
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static convertToObject(permissions: Array<PermissionString>): PermissionObject {
|
||||
if (permissions.includes("*")) {
|
||||
return {
|
||||
admin: true,
|
||||
};
|
||||
}
|
||||
let output: PermissionObject = {};
|
||||
let splitPermissions = permissions.map((e) => e.split(".")) as Array<
|
||||
[PermissionSection, PermissionModule | PermissionType | "*", PermissionType | "*"]
|
||||
>;
|
||||
for (let split of splitPermissions) {
|
||||
if (!output[split[0]]) {
|
||||
output[split[0]] = {};
|
||||
}
|
||||
if (split[1] == "*" || output[split[0]].all == "*") {
|
||||
output[split[0]] = { all: "*" };
|
||||
} else if (permissionTypes.includes(split[1] as PermissionType)) {
|
||||
if (!output[split[0]].all || !Array.isArray(output[split[0]].all)) {
|
||||
output[split[0]].all = [];
|
||||
}
|
||||
const permissionIndex = permissionTypes.indexOf(split[1] as PermissionType);
|
||||
const appliedPermissions = permissionTypes.slice(0, permissionIndex + 1);
|
||||
output[split[0]].all = appliedPermissions;
|
||||
} else {
|
||||
if (split[2] == "*" || output[split[0]][split[1] as PermissionModule] == "*") {
|
||||
output[split[0]][split[1] as PermissionModule] = "*";
|
||||
} else {
|
||||
if (
|
||||
!output[split[0]][split[1] as PermissionModule] ||
|
||||
!Array.isArray(output[split[0]][split[1] as PermissionModule])
|
||||
) {
|
||||
output[split[0]][split[1] as PermissionModule] = [];
|
||||
}
|
||||
const permissionIndex = permissionTypes.indexOf(split[2] as PermissionType);
|
||||
const appliedPermissions = permissionTypes.slice(0, permissionIndex + 1);
|
||||
output[split[0]][split[1] as PermissionModule] = appliedPermissions;
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
static convertToStringArray(permissions: PermissionObject): Array<PermissionString> {
|
||||
if (permissions.admin) {
|
||||
return ["*"];
|
||||
}
|
||||
let output: Array<PermissionString> = [];
|
||||
let sections = Object.keys(permissions) as Array<PermissionSection>;
|
||||
for (let section of sections) {
|
||||
if (permissions[section].all) {
|
||||
let types = permissions[section].all;
|
||||
if (types == "*") {
|
||||
output.push(`${section}.*`);
|
||||
} else {
|
||||
for (let type of types) {
|
||||
output.push(`${section}.${type}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let modules = Object.keys(permissions[section]) as Array<PermissionModule>;
|
||||
for (let module of modules) {
|
||||
let types = permissions[section][module];
|
||||
if (types == "*") {
|
||||
output.push(`${section}.${module}.*`);
|
||||
} else {
|
||||
for (let type of types) {
|
||||
output.push(`${section}.${module}.${type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
static getWhatToAdd() {}
|
||||
|
||||
static getWhatToRemove() {}
|
||||
}
|
|
@ -9,7 +9,7 @@ declare global {
|
|||
export interface Request {
|
||||
userId: string;
|
||||
username: string;
|
||||
rights: Array<string>;
|
||||
rights: PermissionObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ dataSource.initialize();
|
|||
|
||||
const app = express();
|
||||
import router from "./routes/index";
|
||||
import { PermissionObject } from "./type/permissionTypes";
|
||||
router(app);
|
||||
app.listen(SERVER_PORT, () => {
|
||||
console.log(`listening on *:${SERVER_PORT}`);
|
||||
|
|
46
src/migrations/1724661484664-permissions.ts
Normal file
46
src/migrations/1724661484664-permissions.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm";
|
||||
|
||||
export class Permissions1724661484664 implements MigrationInterface {
|
||||
name = "Permissions1724661484664";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: "permission",
|
||||
columns: [
|
||||
{
|
||||
name: "permission",
|
||||
type: "varchar",
|
||||
length: "255",
|
||||
isPrimary: true,
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: "userId",
|
||||
type: "int",
|
||||
isPrimary: true,
|
||||
isNullable: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
||||
await queryRunner.createForeignKey(
|
||||
"permission",
|
||||
new TableForeignKey({
|
||||
columnNames: ["userId"],
|
||||
referencedColumnNames: ["id"],
|
||||
referencedTableName: "user",
|
||||
onDelete: "No Action",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
const table = await queryRunner.getTable("permission");
|
||||
const foreignKey = table.foreignKeys.find((fk) => fk.columnNames.indexOf("userId") !== -1);
|
||||
await queryRunner.dropForeignKey("permission", foreignKey);
|
||||
await queryRunner.dropTable("permission");
|
||||
}
|
||||
}
|
|
@ -22,7 +22,7 @@ router.post(
|
|||
);
|
||||
|
||||
router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => {
|
||||
await finishInvite(req, res);
|
||||
await finishInvite(req, res, true);
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
24
src/service/permissionService.ts
Normal file
24
src/service/permissionService.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { dataSource } from "../data-source";
|
||||
import { permission } from "../entity/permission";
|
||||
import InternalException from "../exceptions/internalException";
|
||||
|
||||
export default abstract class PermissionService {
|
||||
/**
|
||||
* @description get permission by user
|
||||
* @param user number
|
||||
* @returns {Promise<Array<permission>>}
|
||||
*/
|
||||
static async getByUser(userId: number): Promise<Array<permission>> {
|
||||
return await dataSource
|
||||
.getRepository(permission)
|
||||
.createQueryBuilder("permission")
|
||||
.where("permission.userId = :userId", { userId: userId })
|
||||
.getMany()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException("permission not found by user");
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,11 +1,16 @@
|
|||
import { PermissionObject } from "./permissionTypes";
|
||||
|
||||
export type JWTData = {
|
||||
[key: string]: string | number | Array<string>;
|
||||
[key: string]: string | number | PermissionObject;
|
||||
};
|
||||
|
||||
export type JWTToken = {
|
||||
userId: number;
|
||||
mail: string;
|
||||
username: string;
|
||||
rights: Array<string>;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
permissions: PermissionObject;
|
||||
} & JWTData;
|
||||
|
||||
export type JWTRefresh = {
|
||||
|
|
24
src/type/permissionTypes.ts
Normal file
24
src/type/permissionTypes.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
export type PermissionSection = "club" | "settings" | "user";
|
||||
|
||||
export type PermissionModule = "protocoll" | "user";
|
||||
|
||||
export type PermissionType = "read" | "create" | "update" | "delete";
|
||||
|
||||
export type PermissionString =
|
||||
| `${PermissionSection}.${PermissionModule}.${PermissionType}` // für spezifische Berechtigungen
|
||||
| `${PermissionSection}.${PermissionModule}.*` // für alle Berechtigungen in einem Modul
|
||||
| `${PermissionSection}.${PermissionType}` // für spezifische Berechtigungen in einem Abschnitt
|
||||
| `${PermissionSection}.*` // für alle Berechtigungen in einem Abschnitt
|
||||
| "*"; // für Admin
|
||||
|
||||
export type PermissionObject = {
|
||||
[section in PermissionSection]?: {
|
||||
[module in PermissionModule]?: Array<PermissionType> | "*";
|
||||
} & { all?: Array<PermissionType> | "*" };
|
||||
} & {
|
||||
admin?: boolean;
|
||||
};
|
||||
|
||||
export const permissionSections: Array<PermissionSection> = ["club", "settings", "user"];
|
||||
export const permissionModules: Array<PermissionModule> = ["protocoll", "user"];
|
||||
export const permissionTypes: Array<PermissionType> = ["read", "create", "update", "delete"];
|
1
src/viewmodel/permissionViewModel.ts
Normal file
1
src/viewmodel/permissionViewModel.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export interface PermissionViewModel {}
|
Loading…
Reference in a new issue