base structure
transfered from ff admin
This commit is contained in:
parent
e37c4e90c4
commit
1d56c7f798
101 changed files with 9773 additions and 2 deletions
6
.dockerignore
Normal file
6
.dockerignore
Normal file
|
@ -0,0 +1,6 @@
|
|||
# NodeJs
|
||||
node_modules/
|
||||
dist/
|
||||
.git/
|
||||
files/
|
||||
.env
|
47
.env.example
Normal file
47
.env.example
Normal file
|
@ -0,0 +1,47 @@
|
|||
DB_TYPE = (mysql|sqlite|postgres) # default ist mysql
|
||||
|
||||
## BSP für mysql
|
||||
DB_PORT = 3306
|
||||
DB_HOST = database_host
|
||||
DB_NAME = database_name
|
||||
DB_USERNAME = database_username
|
||||
DB_PASSWORD = database_password
|
||||
|
||||
## BSP für postgres
|
||||
DB_PORT = 5432
|
||||
DB_HOST = database_host
|
||||
DB_NAME = database_name
|
||||
DB_USERNAME = database_username
|
||||
DB_PASSWORD = database_password
|
||||
|
||||
## BSP für sqlite
|
||||
DB_HOST = filename.db
|
||||
|
||||
SERVER_PORT = portnumber
|
||||
|
||||
JWT_SECRET = ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 # besitzt default
|
||||
JWT_EXPIRATION = [0-9]*(y|d|h|m|s) # default ist 15m
|
||||
REFRESH_EXPIRATION = [0-9]*(y|d|h|m|s) # default ist 1d
|
||||
PWA_REFRESH_EXPIRATION = [0-9]*(y|d|h|m|s) # default ist 5d
|
||||
|
||||
MAIL_USERNAME = mail_username
|
||||
MAIL_PASSWORD = mail_password
|
||||
MAIL_HOST = mail_hoststring
|
||||
MAIL_PORT = mail_portnumber # default ist 587
|
||||
MAIL_SECURE = (true|false) # true für port 465, false für anders gewählten port
|
||||
|
||||
CLUB_NAME = clubname #default FF Admin
|
||||
CLUB_WEBSITE = https://my-club-website-url #optional, muss aber mit http:// oder https:// beginnen
|
||||
|
||||
BACKUP_INTERVAL = number of days (min 1) # default 1
|
||||
BACKUP_COPIES = number of parallel copies # default 7
|
||||
BACKUP_AUTO_RESTORE = (true|false) # default ist true
|
||||
|
||||
USE_SECURITY_STRICT_LIMIT = (true|false) # default ist true
|
||||
SECURITY_STRICT_LIMIT_WINDOW = [0-9]*(y|d|h|m|s) # default ist 15m
|
||||
SECURITY_STRICT_LIMIT_REQUEST_COUNT = strict_request_count # default ist 15
|
||||
USE_SECURITY_LIMIT = (true|false) # default ist true
|
||||
SECURITY_LIMIT_WINDOW = [0-9]*(y|d|h|m|s) # default ist 1m
|
||||
SECURITY_LIMIT_REQUEST_COUNT = request_count # default ist 500
|
||||
|
||||
TRUST_PROXY = <boolean|number|ip|ip1,ip2,...> # wenn leer, wird dieser Wert nicht angewendet.
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -130,3 +130,7 @@ dist
|
|||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
files
|
||||
|
||||
.idea
|
||||
*.db
|
5
.prettierrc
Normal file
5
.prettierrc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"printWidth": 120
|
||||
}
|
46
Dockerfile
Normal file
46
Dockerfile
Normal file
|
@ -0,0 +1,46 @@
|
|||
FROM node:18-alpine AS build
|
||||
|
||||
RUN apk add --no-cache \
|
||||
chromium \
|
||||
nss \
|
||||
freetype \
|
||||
harfbuzz \
|
||||
ca-certificates \
|
||||
ttf-freefont
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . /app
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM node:18-alpine AS prod
|
||||
|
||||
RUN apk add --no-cache \
|
||||
chromium \
|
||||
nss \
|
||||
freetype \
|
||||
harfbuzz \
|
||||
ca-certificates \
|
||||
ttf-freefont
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN mkdir -p /app/files
|
||||
|
||||
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
||||
|
||||
COPY --from=build /app/src/templates /app/src/templates
|
||||
COPY --from=build /app/dist /app/dist
|
||||
COPY --from=build /app/node_modules /app/node_modules
|
||||
COPY --from=build /app/package.json /app/package.json
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
CMD [ "npm", "run", "start" ]
|
119
README.md
119
README.md
|
@ -1,3 +1,118 @@
|
|||
# ff-operation-server
|
||||
# ff-admin-server
|
||||
|
||||
Einsatzverwaltung für Feuerwehren und Vereine.
|
||||
Administration für Feuerwehren und Vereine (Backend).
|
||||
|
||||
## Einleitung
|
||||
|
||||
Dieses Projekt, `ff-admin-server`, ist das Backend zur Verwaltung von Mitgliederdaten. Die zugehörige Webapp ist im Repository [ff-admin-ui](https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin) zu finden.
|
||||
|
||||
Eine Demo zusammen mit der `ff-admin` finden Sie unter [https://admin-demo.ff-admin.de](https://admin-demo.ff-admin.de).
|
||||
|
||||
## Installation
|
||||
|
||||
Das Image exposed nur den Port 5000. Die Env-Variable SERVER_PORT kann nur im lokal ausführenden dev-Kontext verwendet werden.
|
||||
|
||||
### Docker Compose Setup
|
||||
|
||||
Um den Container hochzufahren, erstellen Sie eine `docker-compose.yml` Datei mit folgendem Inhalt:
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
ff-admin-server:
|
||||
image: docker.registry.jk-effects.cloud/ehrenamt/ff-admin/server:latest
|
||||
container_name: ff_member_administration_server
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- DB_TYPE=<mysql|sqlite|postgres> # default ist auf mysql gesetzt
|
||||
- DB_HOST=ff-db
|
||||
- DB_PORT=<number> # default ist auf 3306 gesetzt
|
||||
- DB_NAME=ffadmin
|
||||
- DB_USERNAME=administration_backend
|
||||
- DB_PASSWORD=<dbuserpasswd>
|
||||
- JWT_SECRET=<tobemodified>
|
||||
- JWT_EXPIRATION=<number[m|d] - bsp.:15m> # default ist auf 15m gesetzt
|
||||
- REFRESH_EXPIRATION=<number[m|d] - bsp.:1d> # default ist auf 1d gesetzt
|
||||
- PWA_REFRESH_EXPIRATION=<number[m|d] - bsp.:5d> # default ist auf 5d gesetzt
|
||||
- MAIL_USERNAME=<mailadress|username>
|
||||
- MAIL_PASSWORD=<password>
|
||||
- MAIL_HOST=<url>
|
||||
- MAIL_PORT=<port> # default ist auf 587 gesetzt
|
||||
- MAIL_SECURE=<boolean> # default ist auf false gesetzt
|
||||
- CLUB_NAME=<tobemodified> # default ist auf FF Admin gesetzt
|
||||
- CLUB_WEBSITE=<tobemodified>
|
||||
- BACKUP_INTERVAL=<number of days (min. 1)> # alle x Tage, sonst keine
|
||||
- BACKUP_COPIES=<number of parallel copies> # Anzahl parallel bestehender Backups
|
||||
- BACKUP_AUTO_RESTORE=<boolean> # default ist auf true gesetzt
|
||||
- USE_SECURITY_STRICT_LIMIT = (true|false) # default ist true
|
||||
- SECURITY_STRICT_LIMIT_WINDOW = [0-9]*(y|d|h|m|s) # default ist 15
|
||||
- SECURITY_STRICT_LIMIT_REQUEST_COUNT = strict_request_count # default ist 15
|
||||
- USE_SECURITY_LIMIT = (true|false) # default ist true
|
||||
- SECURITY_LIMIT_WINDOW = [0-9]*(y|d|h|m|s) # default ist 1m
|
||||
- SECURITY_LIMIT_REQUEST_COUNT = request_count # default ist 500
|
||||
- TRUST_PROXY = <boolean|number|ip|ip1,ip2,...> # wenn leer, wird dieser Wert nicht angewendet.
|
||||
volumes:
|
||||
- <volume|local path>:/app/files
|
||||
networks:
|
||||
- ff_internal
|
||||
depends_on:
|
||||
- ff-db
|
||||
|
||||
ff-db:
|
||||
image: mariadb:11.2
|
||||
container_name: ff_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- MYSQL_DATABASE=ffadmin
|
||||
- MYSQL_USER=administration_backend
|
||||
- MYSQL_PASSWORD=<dbuserpasswd>
|
||||
- MYSQL_ROOT_PASSWORD=<dbrootpasswd>
|
||||
volumes:
|
||||
- <volume|local path>:/var/lib/mysql
|
||||
networks:
|
||||
- ff_internal
|
||||
# OR
|
||||
image: postgres:16
|
||||
container_name: ff_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_DB=ffadmin
|
||||
- POSTGRES_USER=administration_backend
|
||||
- POSTGRES_PASSWORD=<dbuserpasswd>
|
||||
volumes:
|
||||
- <volume|local path>:/var/lib/postgresql/data
|
||||
networks:
|
||||
- ff_internal
|
||||
|
||||
networks:
|
||||
ff_internal:
|
||||
```
|
||||
|
||||
Die Verwendung von postgres wird aufgrund des Verhaltens bei Datenbank-Update-Fehlern empfohlen.
|
||||
|
||||
Die Verwendung von SQLite wird nur für die Entwicklung oder lokale Tests empfohlen.
|
||||
|
||||
Führen Sie dann den folgenden Befehl im Verzeichnis der compose-Datei aus, um den Container zu starten:
|
||||
|
||||
```sh
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Manuelle Installation
|
||||
|
||||
Klonen Sie dieses Repository und installieren Sie die Abhängigkeiten:
|
||||
|
||||
```sh
|
||||
git clone https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin-server.git
|
||||
cd ff-admin-server
|
||||
npm install
|
||||
npm run build
|
||||
npm run start
|
||||
```
|
||||
|
||||
## Fragen und Wünsche
|
||||
|
||||
Bei Fragen, Anregungen oder Wünschen können Sie sich gerne melden.\
|
||||
Wir freuen uns über Ihr Feedback und helfen Ihnen gerne weiter.\
|
||||
Schreiben Sie dafür eine Mail an julian.krauser@jk-effects.com.
|
||||
|
|
4829
package-lock.json
generated
Normal file
4829
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
74
package.json
Normal file
74
package.json
Normal file
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"name": "ff-operation-server",
|
||||
"version": "0.0.0",
|
||||
"description": "Feuerwehr/Verein Einsatzverwaltung Server",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"start_ts": "ts-node src/index.ts",
|
||||
"typeorm": "typeorm-ts-node-commonjs",
|
||||
"migrate": "set DBMODE=migration && npx typeorm-ts-node-commonjs migration:generate ./src/migrations/%npm_config_name% -d ./src/data-source.ts",
|
||||
"synchronize-database": "set DBMODE=update-database && npx typeorm-ts-node-commonjs schema:sync -d ./src/data-source.ts",
|
||||
"update-database": "set DBMODE=update-database && npx typeorm-ts-node-commonjs migration:run -d ./src/data-source.ts",
|
||||
"revert-database": "set DBMODE=update-database && npx typeorm-ts-node-commonjs migration:revert -d ./src/data-source.ts",
|
||||
"build": "tsc",
|
||||
"start": "node .",
|
||||
"dev": "npm run build && set NODE_ENV=development && npm run start"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://forgejo.jk-effects.cloud/Ehrenamt/ff-operation-server.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Feuerwehr"
|
||||
],
|
||||
"author": "JK Effects",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^5.0.0-beta.3",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"express-validator": "^7.2.1",
|
||||
"handlebars": "^4.7.8",
|
||||
"helmet": "^8.0.0",
|
||||
"ics": "^3.8.1",
|
||||
"ip": "^2.0.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash.uniqby": "^4.7.0",
|
||||
"moment": "^2.30.1",
|
||||
"morgan": "^1.10.0",
|
||||
"ms": "^2.1.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql": "^2.18.1",
|
||||
"node-schedule": "^2.1.1",
|
||||
"nodemailer": "^6.10.0",
|
||||
"pg": "^8.13.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rss-parser": "^3.13.0",
|
||||
"socket.io": "^4.7.5",
|
||||
"speakeasy": "^2.0.0",
|
||||
"sqlite3": "^5.1.7",
|
||||
"typeorm": "^0.3.20",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.14",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/lodash.uniqby": "^4.7.9",
|
||||
"@types/morgan": "^1.9.9",
|
||||
"@types/ms": "^0.7.34",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/mysql": "^2.15.21",
|
||||
"@types/node": "^16.18.41",
|
||||
"@types/node-schedule": "^2.1.6",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/qrcode": "~1.5.5",
|
||||
"@types/speakeasy": "^2.0.10",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"ts-node": "10.7.0",
|
||||
"typescript": "^4.5.2"
|
||||
}
|
||||
}
|
22
src/command/configuration/member/memberCommand.ts
Normal file
22
src/command/configuration/member/memberCommand.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
export interface CreateMemberCommand {
|
||||
salutationId: number;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
nameaffix: string;
|
||||
birthdate: Date;
|
||||
internalId?: string;
|
||||
}
|
||||
|
||||
export interface UpdateMemberCommand {
|
||||
id: string;
|
||||
salutationId: number;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
nameaffix: string;
|
||||
birthdate: Date;
|
||||
internalId?: string;
|
||||
}
|
||||
|
||||
export interface DeleteMemberCommand {
|
||||
id: string;
|
||||
}
|
70
src/command/configuration/member/memberCommandHandler.ts
Normal file
70
src/command/configuration/member/memberCommandHandler.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
import { dataSource } from "../../../data-source";
|
||||
import { member } from "../../../entity/configuration/member";
|
||||
import DatabaseActionException from "../../../exceptions/databaseActionException";
|
||||
import { CreateMemberCommand, DeleteMemberCommand, UpdateMemberCommand } from "./memberCommand";
|
||||
|
||||
export default abstract class MemberCommandHandler {
|
||||
/**
|
||||
* @description create member
|
||||
* @param {CreateMemberCommand} createMember
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
static async create(createMember: CreateMemberCommand): Promise<number> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(member)
|
||||
.values({
|
||||
firstname: createMember.firstname,
|
||||
lastname: createMember.lastname,
|
||||
nameaffix: createMember.nameaffix,
|
||||
})
|
||||
.execute()
|
||||
.then((result) => {
|
||||
return result.identifiers[0].id;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("CREATE", "member", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description update member
|
||||
* @param {UpdateMemberCommand} updateMember
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async update(updateMember: UpdateMemberCommand): Promise<void> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.update(member)
|
||||
.set({
|
||||
firstname: updateMember.firstname,
|
||||
lastname: updateMember.lastname,
|
||||
nameaffix: updateMember.nameaffix,
|
||||
})
|
||||
.where("id = :id", { id: updateMember.id })
|
||||
.execute()
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("UPDATE", "member", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description delete member
|
||||
* @param {DeleteMemberCommand} deleteMember
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async delete(deleteMember: DeleteMemberCommand): Promise<void> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(member)
|
||||
.where("id = :id", { id: deleteMember.id })
|
||||
.execute()
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("DELETE", "member", err);
|
||||
});
|
||||
}
|
||||
}
|
12
src/command/management/role/roleCommand.ts
Normal file
12
src/command/management/role/roleCommand.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
export interface CreateRoleCommand {
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface UpdateRoleCommand {
|
||||
id: number;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface DeleteRoleCommand {
|
||||
id: number;
|
||||
}
|
67
src/command/management/role/roleCommandHandler.ts
Normal file
67
src/command/management/role/roleCommandHandler.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { dataSource } from "../../../data-source";
|
||||
import { role } from "../../../entity/management/role";
|
||||
import DatabaseActionException from "../../../exceptions/databaseActionException";
|
||||
import InternalException from "../../../exceptions/internalException";
|
||||
import { CreateRoleCommand, DeleteRoleCommand, UpdateRoleCommand } from "./roleCommand";
|
||||
|
||||
export default abstract class RoleCommandHandler {
|
||||
/**
|
||||
* @description create role
|
||||
* @param {CreateRoleCommand} createRole
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
static async create(createRole: CreateRoleCommand): Promise<number> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(role)
|
||||
.values({
|
||||
role: createRole.role,
|
||||
})
|
||||
.execute()
|
||||
.then((result) => {
|
||||
return result.identifiers[0].id;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("CREATE", "role", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description update role
|
||||
* @param {UpdateRoleCommand} updateRole
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async update(updateRole: UpdateRoleCommand): Promise<void> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.update(role)
|
||||
.set({
|
||||
role: updateRole.role,
|
||||
})
|
||||
.where("id = :id", { id: updateRole.id })
|
||||
.execute()
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("UPDATE", "role", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description delete role
|
||||
* @param {DeleteRoleCommand} deleteRole
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async delete(deleteRole: DeleteRoleCommand): Promise<void> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(role)
|
||||
.where("id = :id", { id: deleteRole.id })
|
||||
.execute()
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("DELETE", "role", err);
|
||||
});
|
||||
}
|
||||
}
|
16
src/command/management/role/rolePermissionCommand.ts
Normal file
16
src/command/management/role/rolePermissionCommand.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { PermissionString } from "../../../type/permissionTypes";
|
||||
|
||||
export interface CreateRolePermissionCommand {
|
||||
permission: PermissionString;
|
||||
roleId: number;
|
||||
}
|
||||
|
||||
export interface DeleteRolePermissionCommand {
|
||||
permission: PermissionString;
|
||||
roleId: number;
|
||||
}
|
||||
|
||||
export interface UpdateRolePermissionsCommand {
|
||||
roleId: number;
|
||||
permissions: Array<PermissionString>;
|
||||
}
|
75
src/command/management/role/rolePermissionCommandHandler.ts
Normal file
75
src/command/management/role/rolePermissionCommandHandler.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { DeleteResult, EntityManager, InsertResult } from "typeorm";
|
||||
import { dataSource } from "../../../data-source";
|
||||
import { rolePermission } from "../../../entity/management/role_permission";
|
||||
import InternalException from "../../../exceptions/internalException";
|
||||
import RoleService from "../../../service/management/roleService";
|
||||
import {
|
||||
CreateRolePermissionCommand,
|
||||
DeleteRolePermissionCommand,
|
||||
UpdateRolePermissionsCommand,
|
||||
} from "./rolePermissionCommand";
|
||||
import PermissionHelper from "../../../helpers/permissionHelper";
|
||||
import RolePermissionService from "../../../service/management/rolePermissionService";
|
||||
import { PermissionString } from "../../../type/permissionTypes";
|
||||
import DatabaseActionException from "../../../exceptions/databaseActionException";
|
||||
|
||||
export default abstract class RolePermissionCommandHandler {
|
||||
/**
|
||||
* @description update role permissions
|
||||
* @param {UpdateRolePermissionsCommand} updateRolePermissions
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async updatePermissions(updateRolePermissions: UpdateRolePermissionsCommand): Promise<void> {
|
||||
let currentPermissions = (await RolePermissionService.getByRole(updateRolePermissions.roleId)).map(
|
||||
(r) => r.permission
|
||||
);
|
||||
return await dataSource.manager
|
||||
.transaction(async (manager) => {
|
||||
let newPermissions = PermissionHelper.getWhatToAdd(currentPermissions, updateRolePermissions.permissions);
|
||||
let removePermissions = PermissionHelper.getWhatToRemove(currentPermissions, updateRolePermissions.permissions);
|
||||
if (newPermissions.length != 0) {
|
||||
await this.updatePermissionsAdd(manager, updateRolePermissions.roleId, newPermissions);
|
||||
}
|
||||
if (removePermissions.length != 0) {
|
||||
await this.updatePermissionsRemove(manager, updateRolePermissions.roleId, removePermissions);
|
||||
}
|
||||
})
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("UPDATE", "rolePermissions", err);
|
||||
});
|
||||
}
|
||||
|
||||
private static async updatePermissionsAdd(
|
||||
manager: EntityManager,
|
||||
roleId: number,
|
||||
permissions: Array<PermissionString>
|
||||
): Promise<InsertResult> {
|
||||
return await manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(rolePermission)
|
||||
.values(
|
||||
permissions.map((p) => ({
|
||||
permission: p,
|
||||
roleId: roleId,
|
||||
}))
|
||||
)
|
||||
.orIgnore()
|
||||
.execute();
|
||||
}
|
||||
|
||||
private static async updatePermissionsRemove(
|
||||
manager: EntityManager,
|
||||
roleId: number,
|
||||
permissions: Array<PermissionString>
|
||||
): Promise<DeleteResult> {
|
||||
return await manager
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(rolePermission)
|
||||
.where("roleId = :id", { id: roleId })
|
||||
.andWhere("permission IN (:...permission)", { permission: permissions })
|
||||
.execute();
|
||||
}
|
||||
}
|
12
src/command/management/user/inviteCommand.ts
Normal file
12
src/command/management/user/inviteCommand.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
export interface CreateInviteCommand {
|
||||
mail: string;
|
||||
username: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
secret: string;
|
||||
}
|
||||
|
||||
export interface DeleteInviteCommand {
|
||||
token: string;
|
||||
mail: string;
|
||||
}
|
75
src/command/management/user/inviteCommandHandler.ts
Normal file
75
src/command/management/user/inviteCommandHandler.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import { dataSource } from "../../../data-source";
|
||||
import { invite } from "../../../entity/management/invite";
|
||||
import DatabaseActionException from "../../../exceptions/databaseActionException";
|
||||
import InternalException from "../../../exceptions/internalException";
|
||||
import { StringHelper } from "../../../helpers/stringHelper";
|
||||
import { CreateInviteCommand, DeleteInviteCommand } from "./inviteCommand";
|
||||
|
||||
export default abstract class InviteCommandHandler {
|
||||
/**
|
||||
* @description create user
|
||||
* @param CreateInviteCommand
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
static async create(createInvite: CreateInviteCommand): Promise<string> {
|
||||
const token = StringHelper.random(32);
|
||||
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(invite)
|
||||
.values({
|
||||
mail: createInvite.mail,
|
||||
token: token,
|
||||
username: createInvite.username,
|
||||
firstname: createInvite.firstname,
|
||||
lastname: createInvite.lastname,
|
||||
secret: createInvite.secret,
|
||||
})
|
||||
.orUpdate(["firstName", "lastName", "token", "secret"], ["mail"])
|
||||
.execute()
|
||||
.then((result) => {
|
||||
return token;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("CREATE", "invite", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description delete invite by mail and token
|
||||
* @param DeleteInviteCommand
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
static async deleteByTokenAndMail(deleteInvite: DeleteInviteCommand): Promise<any> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(invite)
|
||||
.where("invite.token = :token", { token: deleteInvite.token })
|
||||
.andWhere("invite.mail = :mail", { mail: deleteInvite.mail })
|
||||
.execute()
|
||||
.then((res) => {})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("DELETE", "invite", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description delete invite by mail
|
||||
* @param DeleteByMailInviteCommand
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
static async deleteByMail(mail: string): Promise<any> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(invite)
|
||||
.where("invite.mail = :mail", { mail })
|
||||
.execute()
|
||||
.then((res) => {})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("DELETE", "invite", err);
|
||||
});
|
||||
}
|
||||
}
|
35
src/command/management/user/userCommand.ts
Normal file
35
src/command/management/user/userCommand.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
export interface CreateUserCommand {
|
||||
mail: string;
|
||||
username: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
secret: string;
|
||||
isOwner: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateUserCommand {
|
||||
id: string;
|
||||
mail: string;
|
||||
username: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
}
|
||||
|
||||
export interface UpdateUserSecretCommand {
|
||||
id: string;
|
||||
secret: string;
|
||||
}
|
||||
|
||||
export interface TransferUserOwnerCommand {
|
||||
fromId: string;
|
||||
toId: string;
|
||||
}
|
||||
|
||||
export interface UpdateUserRolesCommand {
|
||||
id: string;
|
||||
roleIds: Array<number>;
|
||||
}
|
||||
|
||||
export interface DeleteUserCommand {
|
||||
id: string;
|
||||
}
|
170
src/command/management/user/userCommandHandler.ts
Normal file
170
src/command/management/user/userCommandHandler.ts
Normal file
|
@ -0,0 +1,170 @@
|
|||
import { EntityManager } from "typeorm";
|
||||
import { dataSource } from "../../../data-source";
|
||||
import { user } from "../../../entity/management/user";
|
||||
import InternalException from "../../../exceptions/internalException";
|
||||
import {
|
||||
CreateUserCommand,
|
||||
DeleteUserCommand,
|
||||
TransferUserOwnerCommand,
|
||||
UpdateUserCommand,
|
||||
UpdateUserRolesCommand,
|
||||
UpdateUserSecretCommand,
|
||||
} from "./userCommand";
|
||||
import UserService from "../../../service/management/userService";
|
||||
import DatabaseActionException from "../../../exceptions/databaseActionException";
|
||||
|
||||
export default abstract class UserCommandHandler {
|
||||
/**
|
||||
* @description create user
|
||||
* @param {CreateUserCommand} createUser
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
static async create(createUser: CreateUserCommand): Promise<string> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(user)
|
||||
.values({
|
||||
username: createUser.username,
|
||||
mail: createUser.mail,
|
||||
firstname: createUser.firstname,
|
||||
lastname: createUser.lastname,
|
||||
secret: createUser.secret,
|
||||
isOwner: createUser.isOwner,
|
||||
})
|
||||
.execute()
|
||||
.then((result) => {
|
||||
return result.identifiers[0].id;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("CREATE", "user", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description update user
|
||||
* @param {UpdateUserCommand} updateUser
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async update(updateUser: UpdateUserCommand): Promise<void> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.update(user)
|
||||
.set({
|
||||
mail: updateUser.mail,
|
||||
firstname: updateUser.firstname,
|
||||
lastname: updateUser.lastname,
|
||||
username: updateUser.username,
|
||||
})
|
||||
.where("id = :id", { id: updateUser.id })
|
||||
.execute()
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("UPDATE", "user", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description update user
|
||||
* @param {UpdateUserSecretCommand} updateUser
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async updateSecret(updateUser: UpdateUserSecretCommand): Promise<void> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.update(user)
|
||||
.set({
|
||||
secret: updateUser.secret,
|
||||
})
|
||||
.where("id = :id", { id: updateUser.id })
|
||||
.execute()
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("UPDATE", "user", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description update user roles
|
||||
* @param {UpdateUserRolesCommand} updateUserRoles
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async updateRoles(updateUserRoles: UpdateUserRolesCommand): Promise<void> {
|
||||
let currentRoles = (await UserService.getAssignedRolesByUserId(updateUserRoles.id)).map((r) => r.id);
|
||||
return await dataSource.manager
|
||||
.transaction(async (manager) => {
|
||||
let newRoles = updateUserRoles.roleIds.filter((r) => !currentRoles.includes(r));
|
||||
let removeRoles = currentRoles.filter((r) => !updateUserRoles.roleIds.includes(r));
|
||||
|
||||
for (let role of newRoles) {
|
||||
await this.updateRolesAdd(manager, updateUserRoles.id, role);
|
||||
}
|
||||
|
||||
for (let role of removeRoles) {
|
||||
await this.updateRolesRemove(manager, updateUserRoles.id, role);
|
||||
}
|
||||
})
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("UPDATE", "userRoles", err);
|
||||
});
|
||||
}
|
||||
|
||||
private static async updateRolesAdd(manager: EntityManager, userId: string, roleId: number): Promise<void> {
|
||||
return await manager.createQueryBuilder().relation(user, "roles").of(userId).add(roleId);
|
||||
}
|
||||
|
||||
private static async updateRolesRemove(manager: EntityManager, userId: string, roleId: number): Promise<void> {
|
||||
return await manager.createQueryBuilder().relation(user, "roles").of(userId).remove(roleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description transfer ownership
|
||||
* @param {TransferUserOwnerCommand} transferOwnership
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async transferOwnership(transferOwnership: TransferUserOwnerCommand): Promise<void> {
|
||||
return await dataSource.manager
|
||||
.transaction(async (manager) => {
|
||||
await manager
|
||||
.createQueryBuilder()
|
||||
.update(user)
|
||||
.set({
|
||||
isOwner: false,
|
||||
})
|
||||
.where("id = :id", { id: transferOwnership.fromId })
|
||||
.execute();
|
||||
|
||||
await manager
|
||||
.createQueryBuilder()
|
||||
.update(user)
|
||||
.set({
|
||||
isOwner: true,
|
||||
})
|
||||
.where("id = :id", { id: transferOwnership.toId })
|
||||
.execute();
|
||||
})
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("ABORT", "transfer owner", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description delete user
|
||||
* @param DeleteUserCommand
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async delete(deleteUser: DeleteUserCommand): Promise<void> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(user)
|
||||
.where("id = :id", { id: deleteUser.id })
|
||||
.execute()
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("DELETE", "user", err);
|
||||
});
|
||||
}
|
||||
}
|
16
src/command/management/user/userPermissionCommand.ts
Normal file
16
src/command/management/user/userPermissionCommand.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { PermissionString } from "../../../type/permissionTypes";
|
||||
|
||||
export interface CreateUserPermissionCommand {
|
||||
permission: PermissionString;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface DeleteUserPermissionCommand {
|
||||
permission: PermissionString;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface UpdateUserPermissionsCommand {
|
||||
userId: string;
|
||||
permissions: Array<PermissionString>;
|
||||
}
|
76
src/command/management/user/userPermissionCommandHandler.ts
Normal file
76
src/command/management/user/userPermissionCommandHandler.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import { DeleteResult, EntityManager, InsertResult } from "typeorm";
|
||||
import { dataSource } from "../../../data-source";
|
||||
import { user } from "../../../entity/management/user";
|
||||
import { userPermission } from "../../../entity/management/user_permission";
|
||||
import InternalException from "../../../exceptions/internalException";
|
||||
import {
|
||||
CreateUserPermissionCommand,
|
||||
DeleteUserPermissionCommand,
|
||||
UpdateUserPermissionsCommand,
|
||||
} from "./userPermissionCommand";
|
||||
import UserPermissionService from "../../../service/management/userPermissionService";
|
||||
import PermissionHelper from "../../../helpers/permissionHelper";
|
||||
import { PermissionString } from "../../../type/permissionTypes";
|
||||
import DatabaseActionException from "../../../exceptions/databaseActionException";
|
||||
|
||||
export default abstract class UserPermissionCommandHandler {
|
||||
/**
|
||||
* @description update user permissions
|
||||
* @param {UpdateUserPermissionsCommand} updateUserPermissions
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async updatePermissions(updateUserPermissions: UpdateUserPermissionsCommand): Promise<void> {
|
||||
let currentPermissions = (await UserPermissionService.getByUser(updateUserPermissions.userId)).map(
|
||||
(r) => r.permission
|
||||
);
|
||||
return await dataSource.manager
|
||||
.transaction(async (manager) => {
|
||||
let newPermissions = PermissionHelper.getWhatToAdd(currentPermissions, updateUserPermissions.permissions);
|
||||
let removePermissions = PermissionHelper.getWhatToRemove(currentPermissions, updateUserPermissions.permissions);
|
||||
|
||||
if (newPermissions.length != 0) {
|
||||
await this.updatePermissionsAdd(manager, updateUserPermissions.userId, newPermissions);
|
||||
}
|
||||
if (removePermissions.length != 0) {
|
||||
await this.updatePermissionsRemove(manager, updateUserPermissions.userId, removePermissions);
|
||||
}
|
||||
})
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("UPDATE", "userPermissions", err);
|
||||
});
|
||||
}
|
||||
|
||||
private static async updatePermissionsAdd(
|
||||
manager: EntityManager,
|
||||
userId: string,
|
||||
permissions: Array<PermissionString>
|
||||
): Promise<InsertResult> {
|
||||
return await manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(userPermission)
|
||||
.values(
|
||||
permissions.map((p) => ({
|
||||
permission: p,
|
||||
userId: userId,
|
||||
}))
|
||||
)
|
||||
.orIgnore()
|
||||
.execute();
|
||||
}
|
||||
|
||||
private static async updatePermissionsRemove(
|
||||
manager: EntityManager,
|
||||
userId: string,
|
||||
permissions: Array<PermissionString>
|
||||
): Promise<DeleteResult> {
|
||||
return await manager
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(userPermission)
|
||||
.where("userId = :id", { id: userId })
|
||||
.andWhere("permission IN (:...permission)", { permission: permissions })
|
||||
.execute();
|
||||
}
|
||||
}
|
9
src/command/refreshCommand.ts
Normal file
9
src/command/refreshCommand.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
export interface CreateRefreshCommand {
|
||||
userId: string;
|
||||
isFromPwa?: boolean;
|
||||
}
|
||||
|
||||
export interface DeleteRefreshCommand {
|
||||
token: string;
|
||||
userId: string;
|
||||
}
|
74
src/command/refreshCommandHandler.ts
Normal file
74
src/command/refreshCommandHandler.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { dataSource } from "../data-source";
|
||||
import { refresh } from "../entity/refresh";
|
||||
import { PWA_REFRESH_EXPIRATION, REFRESH_EXPIRATION } from "../env.defaults";
|
||||
import DatabaseActionException from "../exceptions/databaseActionException";
|
||||
import InternalException from "../exceptions/internalException";
|
||||
import { StringHelper } from "../helpers/stringHelper";
|
||||
import UserService from "../service/management/userService";
|
||||
import { CreateRefreshCommand, DeleteRefreshCommand } from "./refreshCommand";
|
||||
import ms from "ms";
|
||||
|
||||
export default abstract class RefreshCommandHandler {
|
||||
/**
|
||||
* @description create and save refreshToken to user
|
||||
* @param {CreateRefreshCommand} createRefresh
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
static async create(createRefresh: CreateRefreshCommand): Promise<string> {
|
||||
const refreshToken = StringHelper.random(32);
|
||||
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(refresh)
|
||||
.values({
|
||||
token: refreshToken,
|
||||
userId: createRefresh.userId,
|
||||
expiry: createRefresh.isFromPwa
|
||||
? new Date(Date.now() + ms(PWA_REFRESH_EXPIRATION))
|
||||
: new Date(Date.now() + ms(REFRESH_EXPIRATION)),
|
||||
})
|
||||
.execute()
|
||||
.then((result) => {
|
||||
return refreshToken;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("CREATE", "refresh", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description delete refresh by user and token
|
||||
* @param {DeleteRefreshCommand} deleteRefresh
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
static async deleteByToken(deleteRefresh: DeleteRefreshCommand): Promise<any> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(refresh)
|
||||
.where({ token: deleteRefresh.token, userId: deleteRefresh.userId })
|
||||
.execute()
|
||||
.then((res) => {})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("DELETE", "refresh", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description delete expired
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
static async deleteExpired(): Promise<any> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(refresh)
|
||||
.where("refresh.expiry < :expiry", { expiry: new Date() })
|
||||
.execute()
|
||||
.then((res) => {})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("DELETE", "refresh", err);
|
||||
});
|
||||
}
|
||||
}
|
10
src/command/resetCommand.ts
Normal file
10
src/command/resetCommand.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export interface CreateResetCommand {
|
||||
mail: string;
|
||||
username: string;
|
||||
secret: string;
|
||||
}
|
||||
|
||||
export interface DeleteResetCommand {
|
||||
token: string;
|
||||
mail: string;
|
||||
}
|
55
src/command/resetCommandHandler.ts
Normal file
55
src/command/resetCommandHandler.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { dataSource } from "../data-source";
|
||||
import { reset } from "../entity/reset";
|
||||
import DatabaseActionException from "../exceptions/databaseActionException";
|
||||
import InternalException from "../exceptions/internalException";
|
||||
import { StringHelper } from "../helpers/stringHelper";
|
||||
import { CreateResetCommand, DeleteResetCommand } from "./resetCommand";
|
||||
|
||||
export default abstract class ResetCommandHandler {
|
||||
/**
|
||||
* @description create user
|
||||
* @param {CreateResetCommand} createReset
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
static async create(createReset: CreateResetCommand): Promise<string> {
|
||||
const token = StringHelper.random(32);
|
||||
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(reset)
|
||||
.values({
|
||||
token: token,
|
||||
mail: createReset.mail,
|
||||
username: createReset.username,
|
||||
secret: createReset.secret,
|
||||
})
|
||||
.orUpdate(["token", "secret"], ["mail"])
|
||||
.execute()
|
||||
.then((result) => {
|
||||
return token;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("CREATE", "reset", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description delete reset by mail and token
|
||||
* @param {DeleteRefreshCommand} deleteReset
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
static async deleteByTokenAndMail(deleteReset: DeleteResetCommand): Promise<any> {
|
||||
return await dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(reset)
|
||||
.where("reset.token = :token", { token: deleteReset.token })
|
||||
.andWhere("reset.mail = :mail", { mail: deleteReset.mail })
|
||||
.execute()
|
||||
.then((res) => {})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("DELETE", "reset", err);
|
||||
});
|
||||
}
|
||||
}
|
137
src/controller/admin/configuration/memberController.ts
Normal file
137
src/controller/admin/configuration/memberController.ts
Normal file
|
@ -0,0 +1,137 @@
|
|||
import { Request, Response } from "express";
|
||||
import MemberService from "../../../service/configuration/memberService";
|
||||
import MemberFactory from "../../../factory/admin/configuration/member";
|
||||
import {
|
||||
CreateMemberCommand,
|
||||
DeleteMemberCommand,
|
||||
UpdateMemberCommand,
|
||||
} from "../../../command/configuration/member/memberCommand";
|
||||
import MemberCommandHandler from "../../../command/configuration/member/memberCommandHandler";
|
||||
|
||||
/**
|
||||
* @description get all members
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getAllMembers(req: Request, res: Response): Promise<any> {
|
||||
let offset = parseInt((req.query.offset as string) ?? "0");
|
||||
let count = parseInt((req.query.count as string) ?? "25");
|
||||
let search = (req.query.search as string) ?? "";
|
||||
let noLimit = req.query.noLimit === "true";
|
||||
let ids = ((req.query.ids ?? "") as string).split(",").filter((i) => i);
|
||||
|
||||
let [members, total] = await MemberService.getAll({ offset, count, search, noLimit, ids });
|
||||
|
||||
res.json({
|
||||
members: MemberFactory.mapToBase(members),
|
||||
total: total,
|
||||
offset: offset,
|
||||
count: count,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get members by Ids
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getMembersByIds(req: Request, res: Response): Promise<any> {
|
||||
let ids = req.body.ids as Array<string>;
|
||||
|
||||
let [members, total] = await MemberService.getAll({ noLimit: true, ids });
|
||||
|
||||
res.json({
|
||||
members: MemberFactory.mapToBase(members),
|
||||
total: total,
|
||||
offset: 0,
|
||||
count: total,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get member by id
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getMemberById(req: Request, res: Response): Promise<any> {
|
||||
const memberId = req.params.id;
|
||||
let member = await MemberService.getById(memberId);
|
||||
|
||||
res.json(MemberFactory.mapToSingle(member));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description create member
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function createMember(req: Request, res: Response): Promise<any> {
|
||||
const salutationId = parseInt(req.body.salutationId);
|
||||
const firstname = req.body.firstname;
|
||||
const lastname = req.body.lastname;
|
||||
const nameaffix = req.body.nameaffix;
|
||||
const birthdate = req.body.birthdate;
|
||||
const internalId = req.body.internalId || null;
|
||||
|
||||
let createMember: CreateMemberCommand = {
|
||||
salutationId,
|
||||
firstname,
|
||||
lastname,
|
||||
nameaffix,
|
||||
birthdate,
|
||||
internalId,
|
||||
};
|
||||
let memberId = await MemberCommandHandler.create(createMember);
|
||||
|
||||
res.status(200).send(memberId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description update member by id
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function updateMemberById(req: Request, res: Response): Promise<any> {
|
||||
const memberId = req.params.id;
|
||||
const salutationId = parseInt(req.body.salutationId);
|
||||
const firstname = req.body.firstname;
|
||||
const lastname = req.body.lastname;
|
||||
const nameaffix = req.body.nameaffix;
|
||||
const birthdate = req.body.birthdate;
|
||||
const internalId = req.body.internalId || null;
|
||||
|
||||
let updateMember: UpdateMemberCommand = {
|
||||
id: memberId,
|
||||
salutationId,
|
||||
firstname,
|
||||
lastname,
|
||||
nameaffix,
|
||||
birthdate,
|
||||
internalId,
|
||||
};
|
||||
await MemberCommandHandler.update(updateMember);
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description delete member by id
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function deleteMemberById(req: Request, res: Response): Promise<any> {
|
||||
const memberId = req.params.id;
|
||||
|
||||
let deleteMember: DeleteMemberCommand = {
|
||||
id: memberId,
|
||||
};
|
||||
await MemberCommandHandler.delete(deleteMember);
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
126
src/controller/admin/management/backupController.ts
Normal file
126
src/controller/admin/management/backupController.ts
Normal file
|
@ -0,0 +1,126 @@
|
|||
import { Request, Response } from "express";
|
||||
import { FileSystemHelper } from "../../../helpers/fileSystemHelper";
|
||||
import BackupHelper from "../../../helpers/backupHelper";
|
||||
import InternalException from "../../../exceptions/internalException";
|
||||
|
||||
/**
|
||||
* @description get generated backups
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getGeneratedBackups(req: Request, res: Response): Promise<any> {
|
||||
let filesInFolder = FileSystemHelper.getFilesInDirectory(`backup`);
|
||||
|
||||
let sorted = filesInFolder.sort((a, b) => new Date(b.split(".")[0]).getTime() - new Date(a.split(".")[0]).getTime());
|
||||
|
||||
res.json(sorted);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description download backup file
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function downloadBackupFile(req: Request, res: Response): Promise<any> {
|
||||
let filename = req.params.filename;
|
||||
|
||||
let filepath = FileSystemHelper.formatPath("backup", filename);
|
||||
|
||||
res.sendFile(filepath, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get uploaded backups
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getUploadedBackups(req: Request, res: Response): Promise<any> {
|
||||
let filesInFolder = FileSystemHelper.getFilesInDirectory("uploaded-backup");
|
||||
|
||||
let sorted = filesInFolder.sort((a, b) => new Date(b.split("_")[0]).getTime() - new Date(a.split("_")[0]).getTime());
|
||||
|
||||
res.json(sorted);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description download uploaded backup file
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function downloadUploadedBackupFile(req: Request, res: Response): Promise<any> {
|
||||
let filename = req.params.filename;
|
||||
|
||||
let filepath = FileSystemHelper.formatPath("uploaded-backup", filename);
|
||||
|
||||
res.sendFile(filepath, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description create backup manually
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function createManualBackup(req: Request, res: Response): Promise<any> {
|
||||
await BackupHelper.createBackup({});
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description restore backup by selected
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function restoreBackupByLocalFile(req: Request, res: Response): Promise<any> {
|
||||
let filename = req.body.filename;
|
||||
let partial = req.body.partial;
|
||||
let include = req.body.include;
|
||||
let overwrite = req.body.overwrite;
|
||||
|
||||
await BackupHelper.loadBackup({ filename, include, partial, overwrite });
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description restore uploaded backup by selected
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function restoreBackupByUploadedFile(req: Request, res: Response): Promise<any> {
|
||||
let filename = req.body.filename;
|
||||
let partial = req.body.partial;
|
||||
let include = req.body.include;
|
||||
|
||||
await BackupHelper.loadBackup({ filename, path: "uploaded-backup", include, partial });
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description upload backup
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function uploadBackupFile(req: Request, res: Response): Promise<any> {
|
||||
if (!req.file) {
|
||||
throw new InternalException("File upload failed");
|
||||
}
|
||||
res.sendStatus(204);
|
||||
}
|
121
src/controller/admin/management/roleController.ts
Normal file
121
src/controller/admin/management/roleController.ts
Normal file
|
@ -0,0 +1,121 @@
|
|||
import { Request, Response } from "express";
|
||||
import RoleService from "../../../service/management/roleService";
|
||||
import RoleFactory from "../../../factory/admin/management/role";
|
||||
import RolePermissionService from "../../../service/management/rolePermissionService";
|
||||
import PermissionHelper from "../../../helpers/permissionHelper";
|
||||
import { CreateRoleCommand, DeleteRoleCommand, UpdateRoleCommand } from "../../../command/management/role/roleCommand";
|
||||
import RoleCommandHandler from "../../../command/management/role/roleCommandHandler";
|
||||
import { UpdateRolePermissionsCommand } from "../../../command/management/role/rolePermissionCommand";
|
||||
import RolePermissionCommandHandler from "../../../command/management/role/rolePermissionCommandHandler";
|
||||
|
||||
/**
|
||||
* @description get All roles
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getAllRoles(req: Request, res: Response): Promise<any> {
|
||||
let roles = await RoleService.getAll();
|
||||
|
||||
res.json(RoleFactory.mapToBase(roles));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get role by id
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getRoleById(req: Request, res: Response): Promise<any> {
|
||||
const id = parseInt(req.params.id);
|
||||
let role = await RoleService.getById(id);
|
||||
|
||||
res.json(RoleFactory.mapToSingle(role));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get permissions by role
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getRolePermissions(req: Request, res: Response): Promise<any> {
|
||||
const id = parseInt(req.params.id);
|
||||
let permissions = await RolePermissionService.getByRole(id);
|
||||
|
||||
res.json(PermissionHelper.convertToObject(permissions.map((p) => p.permission)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description create new role
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function createRole(req: Request, res: Response): Promise<any> {
|
||||
let role = req.body.role;
|
||||
|
||||
let createRole: CreateRoleCommand = {
|
||||
role: role,
|
||||
};
|
||||
await RoleCommandHandler.create(createRole);
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description update role data
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function updateRole(req: Request, res: Response): Promise<any> {
|
||||
const id = parseInt(req.params.id);
|
||||
let role = req.body.role;
|
||||
|
||||
let updateRole: UpdateRoleCommand = {
|
||||
id: id,
|
||||
role: role,
|
||||
};
|
||||
await RoleCommandHandler.update(updateRole);
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description update role assigned permission strings
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function updateRolePermissions(req: Request, res: Response): Promise<any> {
|
||||
const id = parseInt(req.params.id);
|
||||
let permissions = req.body.permissions;
|
||||
|
||||
let permissionStrings = PermissionHelper.convertToStringArray(permissions);
|
||||
|
||||
let updateRolePermissions: UpdateRolePermissionsCommand = {
|
||||
roleId: id,
|
||||
permissions: permissionStrings,
|
||||
};
|
||||
await RolePermissionCommandHandler.updatePermissions(updateRolePermissions);
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description delete role by id
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function deleteRole(req: Request, res: Response): Promise<any> {
|
||||
const id = parseInt(req.params.id);
|
||||
|
||||
let deleteRole: DeleteRoleCommand = {
|
||||
id: id,
|
||||
};
|
||||
await RoleCommandHandler.delete(deleteRole);
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
166
src/controller/admin/management/userController.ts
Normal file
166
src/controller/admin/management/userController.ts
Normal file
|
@ -0,0 +1,166 @@
|
|||
import { Request, Response } from "express";
|
||||
import UserService from "../../../service/management/userService";
|
||||
import UserFactory from "../../../factory/admin/management/user";
|
||||
import UserPermissionService from "../../../service/management/userPermissionService";
|
||||
import PermissionHelper from "../../../helpers/permissionHelper";
|
||||
import RoleFactory from "../../../factory/admin/management/role";
|
||||
import {
|
||||
DeleteUserCommand,
|
||||
UpdateUserCommand,
|
||||
UpdateUserRolesCommand,
|
||||
} from "../../../command/management/user/userCommand";
|
||||
import UserCommandHandler from "../../../command/management/user/userCommandHandler";
|
||||
import MailHelper from "../../../helpers/mailHelper";
|
||||
import { CLUB_NAME } from "../../../env.defaults";
|
||||
import { UpdateUserPermissionsCommand } from "../../../command/management/user/userPermissionCommand";
|
||||
import UserPermissionCommandHandler from "../../../command/management/user/userPermissionCommandHandler";
|
||||
import BadRequestException from "../../../exceptions/badRequestException";
|
||||
|
||||
/**
|
||||
* @description get All users
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getAllUsers(req: Request, res: Response): Promise<any> {
|
||||
let users = await UserService.getAll();
|
||||
|
||||
res.json(UserFactory.mapToBase(users));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get user by id
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getUserById(req: Request, res: Response): Promise<any> {
|
||||
const id = req.params.id;
|
||||
let user = await UserService.getById(id);
|
||||
|
||||
res.json(UserFactory.mapToSingle(user));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get permissions by user
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getUserPermissions(req: Request, res: Response): Promise<any> {
|
||||
const id = req.params.id;
|
||||
let permissions = await UserPermissionService.getByUser(id);
|
||||
|
||||
res.json(PermissionHelper.convertToObject(permissions.map((p) => p.permission)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get assigned roles by user
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getUserRoles(req: Request, res: Response): Promise<any> {
|
||||
const id = req.params.id;
|
||||
|
||||
let roles = await UserService.getAssignedRolesByUserId(id);
|
||||
|
||||
res.json(RoleFactory.mapToBase(roles));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description update user data
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function updateUser(req: Request, res: Response): Promise<any> {
|
||||
const id = req.params.id;
|
||||
let mail = req.body.mail;
|
||||
let firstname = req.body.firstname;
|
||||
let lastname = req.body.lastname;
|
||||
let username = req.body.username;
|
||||
|
||||
let updateUser: UpdateUserCommand = {
|
||||
id: id,
|
||||
mail: mail,
|
||||
firstname: firstname,
|
||||
lastname: lastname,
|
||||
username: username,
|
||||
};
|
||||
await UserCommandHandler.update(updateUser);
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description update user assigned permission strings
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function updateUserPermissions(req: Request, res: Response): Promise<any> {
|
||||
const id = req.params.id;
|
||||
let permissions = req.body.permissions;
|
||||
|
||||
let permissionStrings = PermissionHelper.convertToStringArray(permissions);
|
||||
|
||||
let updateUserPermissions: UpdateUserPermissionsCommand = {
|
||||
userId: id,
|
||||
permissions: permissionStrings,
|
||||
};
|
||||
await UserPermissionCommandHandler.updatePermissions(updateUserPermissions);
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description update user assigned roles
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function updateUserRoles(req: Request, res: Response): Promise<any> {
|
||||
const id = req.params.id;
|
||||
let roleIds = req.body.roleIds as Array<number>;
|
||||
|
||||
let updateRoles: UpdateUserRolesCommand = {
|
||||
id: id,
|
||||
roleIds: roleIds,
|
||||
};
|
||||
await UserCommandHandler.updateRoles(updateRoles);
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description delete user by id
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function deleteUser(req: Request, res: Response): Promise<any> {
|
||||
const id = req.params.id;
|
||||
|
||||
let { mail, isOwner } = await UserService.getById(id);
|
||||
|
||||
if (isOwner) {
|
||||
throw new BadRequestException("Owner cannot be deleted");
|
||||
}
|
||||
|
||||
let deleteUser: DeleteUserCommand = {
|
||||
id: id,
|
||||
};
|
||||
await UserCommandHandler.delete(deleteUser);
|
||||
|
||||
try {
|
||||
// sendmail
|
||||
await MailHelper.sendMail(
|
||||
mail,
|
||||
`Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`,
|
||||
`Ihr Nutzerkonto des Adminportals wurde erfolgreich gelöscht.`
|
||||
);
|
||||
} catch (error) {}
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
98
src/controller/authController.ts
Normal file
98
src/controller/authController.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
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/management/userService";
|
||||
import speakeasy from "speakeasy";
|
||||
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
|
||||
import RefreshService from "../service/refreshService";
|
||||
|
||||
/**
|
||||
* @description Check authentication status by token
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
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 valid = speakeasy.totp.verify({
|
||||
secret: secret,
|
||||
encoding: "base32",
|
||||
token: totp,
|
||||
window: 2,
|
||||
});
|
||||
|
||||
if (!valid) {
|
||||
throw new UnauthorizedRequestException("Token not valid or expired");
|
||||
}
|
||||
|
||||
let accessToken = await JWTHelper.buildToken(id);
|
||||
|
||||
let refreshCommand: CreateRefreshCommand = {
|
||||
userId: id,
|
||||
isFromPwa: req.isPWA,
|
||||
};
|
||||
let refreshToken = await RefreshCommandHandler.create(refreshCommand);
|
||||
|
||||
res.json({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description logout user by token (invalidate refresh token)
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function logout(req: Request, res: Response): Promise<any> {}
|
||||
|
||||
/**
|
||||
* @description refresh expired token
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function refresh(req: Request, res: Response): Promise<any> {
|
||||
let token = req.body.accessToken;
|
||||
let refresh = req.body.refreshToken;
|
||||
|
||||
const tokenUser = await JWTHelper.decode(token);
|
||||
if (typeof tokenUser == "string" || !tokenUser) {
|
||||
throw new InternalException("process failed");
|
||||
}
|
||||
|
||||
let tokenUserId = (tokenUser as JWTToken).userId;
|
||||
|
||||
let { user } = await RefreshService.getByToken(refresh);
|
||||
|
||||
if (tokenUserId != user.id) {
|
||||
throw new UnauthorizedRequestException("user not identified with token and refresh");
|
||||
}
|
||||
|
||||
let accessToken = await JWTHelper.buildToken(tokenUserId);
|
||||
|
||||
let refreshCommand: CreateRefreshCommand = {
|
||||
userId: tokenUserId,
|
||||
isFromPwa: req.isPWA,
|
||||
};
|
||||
let refreshToken = await RefreshCommandHandler.create(refreshCommand);
|
||||
|
||||
let removeToken: DeleteRefreshCommand = {
|
||||
userId: tokenUserId,
|
||||
token: refresh,
|
||||
};
|
||||
await RefreshCommandHandler.deleteByToken(removeToken);
|
||||
|
||||
res.json({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
});
|
||||
}
|
175
src/controller/inviteController.ts
Normal file
175
src/controller/inviteController.ts
Normal file
|
@ -0,0 +1,175 @@
|
|||
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 } from "../command/refreshCommand";
|
||||
import speakeasy from "speakeasy";
|
||||
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
|
||||
import QRCode from "qrcode";
|
||||
import { CreateUserCommand } from "../command/management/user/userCommand";
|
||||
import UserCommandHandler from "../command/management/user/userCommandHandler";
|
||||
import { CreateInviteCommand, DeleteInviteCommand } from "../command/management/user/inviteCommand";
|
||||
import InviteCommandHandler from "../command/management/user/inviteCommandHandler";
|
||||
import MailHelper from "../helpers/mailHelper";
|
||||
import InviteService from "../service/management/inviteService";
|
||||
import UserService from "../service/management/userService";
|
||||
import CustomRequestException from "../exceptions/customRequestException";
|
||||
import { CLUB_NAME } from "../env.defaults";
|
||||
import { CreateUserPermissionCommand } from "../command/management/user/userPermissionCommand";
|
||||
import UserPermissionCommandHandler from "../command/management/user/userPermissionCommandHandler";
|
||||
import InviteFactory from "../factory/admin/management/invite";
|
||||
|
||||
/**
|
||||
* @description get all invites
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getInvites(req: Request, res: Response): Promise<any> {
|
||||
let invites = await InviteService.getAll();
|
||||
|
||||
res.json(InviteFactory.mapToBase(invites));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description start first user
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function inviteUser(req: Request, res: Response, isInvite: boolean = true): Promise<any> {
|
||||
let origin = req.headers.origin;
|
||||
let username = req.body.username;
|
||||
let mail = req.body.mail;
|
||||
let firstname = req.body.firstname;
|
||||
let lastname = req.body.lastname;
|
||||
|
||||
let users = await UserService.getByMailOrUsername(mail, username);
|
||||
if (users.length == 1) {
|
||||
// username or mail is used
|
||||
if (users[0].username == username && users[0].mail == mail) {
|
||||
throw new CustomRequestException(409, "Username and Mail are already in use");
|
||||
} else if (users[0].username == username) {
|
||||
throw new CustomRequestException(409, "Username is already in use");
|
||||
} else {
|
||||
throw new CustomRequestException(409, "Mail is already in use");
|
||||
}
|
||||
} else if (users.length >= 2) {
|
||||
throw new CustomRequestException(409, "Username and Mail are already in use");
|
||||
}
|
||||
|
||||
var secret = speakeasy.generateSecret({ length: 20, name: `FF Operation ${CLUB_NAME}` });
|
||||
|
||||
let createInvite: CreateInviteCommand = {
|
||||
username: username,
|
||||
mail: mail,
|
||||
firstname: firstname,
|
||||
lastname: lastname,
|
||||
secret: secret.base32,
|
||||
};
|
||||
let token = await InviteCommandHandler.create(createInvite);
|
||||
|
||||
// sendmail
|
||||
await MailHelper.sendMail(
|
||||
mail,
|
||||
`Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`,
|
||||
`Öffne folgenden Link: ${origin}/${isInvite ? "invite" : "setup"}/verify?mail=${mail}&token=${token}`
|
||||
);
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Create first user
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function verifyInvite(req: Request, res: Response): Promise<any> {
|
||||
let mail = req.body.mail;
|
||||
let token = req.body.token;
|
||||
|
||||
let { secret, username } = await InviteService.getByMailAndToken(mail, token);
|
||||
|
||||
const url = `otpauth://totp/FF Operation ${CLUB_NAME}?secret=${secret}`;
|
||||
|
||||
QRCode.toDataURL(url)
|
||||
.then((result) => {
|
||||
res.json({
|
||||
dataUrl: result,
|
||||
otp: secret,
|
||||
username,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException("QRCode not created", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Create first user
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
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;
|
||||
|
||||
let { secret, username, firstname, lastname } = await InviteService.getByMailAndToken(mail, token);
|
||||
|
||||
let valid = speakeasy.totp.verify({
|
||||
secret: secret,
|
||||
encoding: "base32",
|
||||
token: totp,
|
||||
window: 2,
|
||||
});
|
||||
|
||||
if (!valid) {
|
||||
throw new UnauthorizedRequestException("Token not valid or expired");
|
||||
}
|
||||
|
||||
let createUser: CreateUserCommand = {
|
||||
username: username,
|
||||
firstname: firstname,
|
||||
lastname: lastname,
|
||||
mail: mail,
|
||||
secret: secret,
|
||||
isOwner: grantAdmin,
|
||||
};
|
||||
let id = await UserCommandHandler.create(createUser);
|
||||
|
||||
let accessToken = await JWTHelper.buildToken(id);
|
||||
|
||||
let refreshCommand: CreateRefreshCommand = {
|
||||
userId: id,
|
||||
};
|
||||
let refreshToken = await RefreshCommandHandler.create(refreshCommand);
|
||||
|
||||
let deleteInvite: DeleteInviteCommand = {
|
||||
mail: mail,
|
||||
token: token,
|
||||
};
|
||||
await InviteCommandHandler.deleteByTokenAndMail(deleteInvite);
|
||||
|
||||
res.json({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description delete invite by mail
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function deleteInvite(req: Request, res: Response): Promise<any> {
|
||||
const mail = req.params.mail;
|
||||
|
||||
await InviteCommandHandler.deleteByMail(mail);
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
124
src/controller/resetController.ts
Normal file
124
src/controller/resetController.ts
Normal file
|
@ -0,0 +1,124 @@
|
|||
import { Request, Response } from "express";
|
||||
import { JWTHelper } from "../helpers/jwtHelper";
|
||||
import InternalException from "../exceptions/internalException";
|
||||
import RefreshCommandHandler from "../command/refreshCommandHandler";
|
||||
import { CreateRefreshCommand } from "../command/refreshCommand";
|
||||
import speakeasy from "speakeasy";
|
||||
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
|
||||
import QRCode from "qrcode";
|
||||
import { CreateResetCommand, DeleteResetCommand } from "../command/resetCommand";
|
||||
import ResetCommandHandler from "../command/resetCommandHandler";
|
||||
import MailHelper from "../helpers/mailHelper";
|
||||
import ResetService from "../service/resetService";
|
||||
import UserService from "../service/management/userService";
|
||||
import { CLUB_NAME } from "../env.defaults";
|
||||
import { UpdateUserSecretCommand } from "../command/management/user/userCommand";
|
||||
import UserCommandHandler from "../command/management/user/userCommandHandler";
|
||||
|
||||
/**
|
||||
* @description request totp reset
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function startReset(req: Request, res: Response): Promise<any> {
|
||||
let origin = req.headers.origin;
|
||||
let username = req.body.username;
|
||||
|
||||
let { mail } = await UserService.getByUsername(username);
|
||||
|
||||
var secret = speakeasy.generateSecret({ length: 20, name: `FF Operation ${CLUB_NAME}` });
|
||||
|
||||
let createReset: CreateResetCommand = {
|
||||
username: username,
|
||||
mail: mail,
|
||||
secret: secret.base32,
|
||||
};
|
||||
let token = await ResetCommandHandler.create(createReset);
|
||||
|
||||
// sendmail
|
||||
await MailHelper.sendMail(
|
||||
mail,
|
||||
`Email Bestätigung für Einsatzverwaltung Admin-Portal von ${CLUB_NAME}`,
|
||||
`Öffne folgenden Link: ${origin}/reset/reset?mail=${mail}&token=${token}`
|
||||
);
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description verify reset link
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function verifyReset(req: Request, res: Response): Promise<any> {
|
||||
let mail = req.body.mail;
|
||||
let token = req.body.token;
|
||||
|
||||
let { secret } = await ResetService.getByMailAndToken(mail, token);
|
||||
|
||||
const url = `otpauth://totp/FF Operation ${CLUB_NAME}?secret=${secret}`;
|
||||
|
||||
QRCode.toDataURL(url)
|
||||
.then((result) => {
|
||||
res.json({
|
||||
dataUrl: result,
|
||||
otp: secret,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException("QRCode not created", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description finishReset
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function finishReset(req: Request, res: Response): Promise<any> {
|
||||
let mail = req.body.mail;
|
||||
let token = req.body.token;
|
||||
let totp = req.body.totp;
|
||||
|
||||
let { secret, username } = await ResetService.getByMailAndToken(mail, token);
|
||||
|
||||
let valid = speakeasy.totp.verify({
|
||||
secret: secret,
|
||||
encoding: "base32",
|
||||
token: totp,
|
||||
window: 2,
|
||||
});
|
||||
|
||||
if (!valid) {
|
||||
throw new UnauthorizedRequestException("Token not valid or expired");
|
||||
}
|
||||
|
||||
let { id } = await UserService.getByUsername(username);
|
||||
|
||||
let updateUserSecret: UpdateUserSecretCommand = {
|
||||
id,
|
||||
secret,
|
||||
};
|
||||
await UserCommandHandler.updateSecret(updateUserSecret);
|
||||
|
||||
let accessToken = await JWTHelper.buildToken(id);
|
||||
|
||||
let refreshCommand: CreateRefreshCommand = {
|
||||
userId: id,
|
||||
};
|
||||
let refreshToken = await RefreshCommandHandler.create(refreshCommand);
|
||||
|
||||
let deleteReset: DeleteResetCommand = {
|
||||
mail: mail,
|
||||
token: token,
|
||||
};
|
||||
await ResetCommandHandler.deleteByTokenAndMail(deleteReset);
|
||||
|
||||
res.json({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
});
|
||||
}
|
11
src/controller/setupController.ts
Normal file
11
src/controller/setupController.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Request, Response } from "express";
|
||||
|
||||
/**
|
||||
* @description Service is currently not configured
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function isSetup(req: Request, res: Response): Promise<any> {
|
||||
res.sendStatus(204);
|
||||
}
|
121
src/controller/userController.ts
Normal file
121
src/controller/userController.ts
Normal file
|
@ -0,0 +1,121 @@
|
|||
import { Request, Response } from "express";
|
||||
import speakeasy from "speakeasy";
|
||||
import QRCode from "qrcode";
|
||||
import InternalException from "../exceptions/internalException";
|
||||
import { CLUB_NAME } from "../env.defaults";
|
||||
import UserService from "../service/management/userService";
|
||||
import UserFactory from "../factory/admin/management/user";
|
||||
import { TransferUserOwnerCommand, UpdateUserCommand } from "../command/management/user/userCommand";
|
||||
import UserCommandHandler from "../command/management/user/userCommandHandler";
|
||||
import ForbiddenRequestException from "../exceptions/forbiddenRequestException";
|
||||
|
||||
/**
|
||||
* @description get my by id
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getMeById(req: Request, res: Response): Promise<any> {
|
||||
const id = req.userId;
|
||||
let user = await UserService.getById(id);
|
||||
|
||||
res.json(UserFactory.mapToSingle(user));
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get my totp
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getMyTotp(req: Request, res: Response): Promise<any> {
|
||||
const userId = req.userId;
|
||||
|
||||
let { secret } = await UserService.getById(userId);
|
||||
|
||||
const url = `otpauth://totp/FF Operation ${CLUB_NAME}?secret=${secret}`;
|
||||
|
||||
QRCode.toDataURL(url)
|
||||
.then((result) => {
|
||||
res.json({
|
||||
dataUrl: result,
|
||||
otp: secret,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException("QRCode not created", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description verify my totp
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function verifyMyTotp(req: Request, res: Response): Promise<any> {
|
||||
const userId = req.userId;
|
||||
let totp = req.body.totp;
|
||||
|
||||
let { secret } = await UserService.getById(userId);
|
||||
let valid = speakeasy.totp.verify({
|
||||
secret: secret,
|
||||
encoding: "base32",
|
||||
token: totp,
|
||||
window: 2,
|
||||
});
|
||||
|
||||
if (!valid) {
|
||||
throw new InternalException("Token not valid or expired");
|
||||
}
|
||||
res.sendStatus(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description transferOwnership
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function transferOwnership(req: Request, res: Response): Promise<any> {
|
||||
const userId = req.userId;
|
||||
let toId = req.body.toId;
|
||||
|
||||
let { isOwner } = await UserService.getById(userId);
|
||||
if (!isOwner) {
|
||||
throw new ForbiddenRequestException("Action only allowed to owner.");
|
||||
}
|
||||
|
||||
let transfer: TransferUserOwnerCommand = {
|
||||
toId: toId,
|
||||
fromId: userId,
|
||||
};
|
||||
await UserCommandHandler.transferOwnership(transfer);
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description update my data
|
||||
* @param req {Request} Express req object
|
||||
* @param res {Response} Express res object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function updateMe(req: Request, res: Response): Promise<any> {
|
||||
const id = req.userId;
|
||||
let mail = req.body.mail;
|
||||
let firstname = req.body.firstname;
|
||||
let lastname = req.body.lastname;
|
||||
let username = req.body.username;
|
||||
|
||||
let updateUser: UpdateUserCommand = {
|
||||
id: id,
|
||||
mail: mail,
|
||||
firstname: firstname,
|
||||
lastname: lastname,
|
||||
username: username,
|
||||
};
|
||||
await UserCommandHandler.update(updateUser);
|
||||
|
||||
res.sendStatus(204);
|
||||
}
|
34
src/data-source.ts
Normal file
34
src/data-source.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import "dotenv/config";
|
||||
import "reflect-metadata";
|
||||
import { DataSource } from "typeorm";
|
||||
import { DB_HOST, DB_USERNAME, DB_PASSWORD, DB_NAME, DB_TYPE, DB_PORT } from "./env.defaults";
|
||||
|
||||
import { user } from "./entity/management/user";
|
||||
import { refresh } from "./entity/refresh";
|
||||
import { invite } from "./entity/management/invite";
|
||||
import { userPermission } from "./entity/management/user_permission";
|
||||
import { role } from "./entity/management/role";
|
||||
import { rolePermission } from "./entity/management/role_permission";
|
||||
import { member } from "./entity/configuration/member";
|
||||
import { reset } from "./entity/reset";
|
||||
|
||||
import { CreateSchema1739697068682 } from "./migrations/1739697068682-CreateSchema";
|
||||
|
||||
const dataSource = new DataSource({
|
||||
type: DB_TYPE as any,
|
||||
host: DB_HOST,
|
||||
port: DB_PORT,
|
||||
username: DB_USERNAME,
|
||||
password: DB_PASSWORD,
|
||||
database: DB_NAME,
|
||||
synchronize: false,
|
||||
logging: process.env.NODE_ENV ? true : ["schema", "error", "warn", "log", "migration"],
|
||||
bigNumberStrings: false,
|
||||
entities: [user, refresh, invite, reset, userPermission, role, rolePermission, member],
|
||||
migrations: [CreateSchema1739697068682],
|
||||
migrationsRun: true,
|
||||
migrationsTransactionMode: "each",
|
||||
subscribers: [],
|
||||
});
|
||||
|
||||
export { dataSource };
|
16
src/entity/configuration/member.ts
Normal file
16
src/entity/configuration/member.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity()
|
||||
export class member {
|
||||
@PrimaryColumn({ generated: "uuid", type: "varchar" })
|
||||
id: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
firstname: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
lastname: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
nameaffix: string;
|
||||
}
|
22
src/entity/management/invite.ts
Normal file
22
src/entity/management/invite.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity()
|
||||
export class invite {
|
||||
@PrimaryColumn({ type: "varchar", length: 255 })
|
||||
mail: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
token: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
username: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
firstname: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
lastname: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
secret: string;
|
||||
}
|
22
src/entity/management/role.ts
Normal file
22
src/entity/management/role.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { Column, Entity, ManyToMany, OneToMany, PrimaryColumn } from "typeorm";
|
||||
import { user } from "./user";
|
||||
import { rolePermission } from "./role_permission";
|
||||
|
||||
@Entity()
|
||||
export class role {
|
||||
@PrimaryColumn({ generated: "increment", type: "int" })
|
||||
id: number;
|
||||
|
||||
@Column({ type: "varchar", length: 255, unique: true })
|
||||
role: string;
|
||||
|
||||
@ManyToMany(() => user, (user) => user.roles, {
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
onUpdate: "RESTRICT",
|
||||
})
|
||||
users: user[];
|
||||
|
||||
@OneToMany(() => rolePermission, (rolePermission) => rolePermission.role, { cascade: ["insert"] })
|
||||
permissions: rolePermission[];
|
||||
}
|
19
src/entity/management/role_permission.ts
Normal file
19
src/entity/management/role_permission.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm";
|
||||
import { PermissionString } from "../../type/permissionTypes";
|
||||
import { role } from "./role";
|
||||
|
||||
@Entity()
|
||||
export class rolePermission {
|
||||
@PrimaryColumn({ type: "int" })
|
||||
roleId: number;
|
||||
|
||||
@PrimaryColumn({ type: "varchar", length: 255 })
|
||||
permission: PermissionString;
|
||||
|
||||
@ManyToOne(() => role, {
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
onUpdate: "RESTRICT",
|
||||
})
|
||||
role: role;
|
||||
}
|
44
src/entity/management/user.ts
Normal file
44
src/entity/management/user.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { Column, Entity, JoinTable, ManyToMany, OneToMany, PrimaryColumn } from "typeorm";
|
||||
import { role } from "./role";
|
||||
import { userPermission } from "./user_permission";
|
||||
|
||||
@Entity()
|
||||
export class user {
|
||||
@PrimaryColumn({ generated: "uuid", type: "varchar" })
|
||||
id: string;
|
||||
|
||||
@Column({ type: "varchar", unique: true, length: 255 })
|
||||
mail: string;
|
||||
|
||||
@Column({ type: "varchar", unique: true, length: 255 })
|
||||
username: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
firstname: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
lastname: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
secret: string;
|
||||
|
||||
@Column({ type: "boolean", default: false })
|
||||
static: boolean;
|
||||
|
||||
@Column({ type: "boolean", default: false })
|
||||
isOwner: boolean;
|
||||
|
||||
@ManyToMany(() => role, (role) => role.users, {
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
onUpdate: "RESTRICT",
|
||||
cascade: ["insert"],
|
||||
})
|
||||
@JoinTable({
|
||||
name: "user_roles",
|
||||
})
|
||||
roles: role[];
|
||||
|
||||
@OneToMany(() => userPermission, (userPermission) => userPermission.user, { cascade: ["insert"] })
|
||||
permissions: userPermission[];
|
||||
}
|
19
src/entity/management/user_permission.ts
Normal file
19
src/entity/management/user_permission.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm";
|
||||
import { user } from "./user";
|
||||
import { PermissionObject, PermissionString } from "../../type/permissionTypes";
|
||||
|
||||
@Entity()
|
||||
export class userPermission {
|
||||
@PrimaryColumn()
|
||||
userId: string;
|
||||
|
||||
@PrimaryColumn({ type: "varchar", length: 255 })
|
||||
permission: PermissionString;
|
||||
|
||||
@ManyToOne(() => user, {
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
onUpdate: "RESTRICT",
|
||||
})
|
||||
user: user;
|
||||
}
|
27
src/entity/management/webapi.ts
Normal file
27
src/entity/management/webapi.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { Column, ColumnType, CreateDateColumn, Entity, OneToMany, PrimaryColumn } from "typeorm";
|
||||
import { webapiPermission } from "./webapi_permission";
|
||||
import { getTypeByORM } from "../../migrations/ormHelper";
|
||||
|
||||
@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: getTypeByORM("datetime").type as ColumnType, nullable: true })
|
||||
lastUsage?: Date;
|
||||
|
||||
@Column({ type: getTypeByORM("date").type as ColumnType, nullable: true })
|
||||
expiry?: Date;
|
||||
|
||||
@OneToMany(() => webapiPermission, (apiPermission) => apiPermission.webapi, { cascade: ["insert"] })
|
||||
permissions: webapiPermission[];
|
||||
}
|
19
src/entity/management/webapi_permission.ts
Normal file
19
src/entity/management/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;
|
||||
}
|
22
src/entity/refresh.ts
Normal file
22
src/entity/refresh.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { Column, ColumnType, Entity, ManyToOne, PrimaryColumn } from "typeorm";
|
||||
import { user } from "./management/user";
|
||||
import { getTypeByORM } from "../migrations/ormHelper";
|
||||
|
||||
@Entity()
|
||||
export class refresh {
|
||||
@PrimaryColumn({ type: "varchar", length: 255 })
|
||||
token: string;
|
||||
|
||||
@PrimaryColumn()
|
||||
userId: string;
|
||||
|
||||
@Column({ type: getTypeByORM("datetime").type as ColumnType })
|
||||
expiry: Date;
|
||||
|
||||
@ManyToOne(() => user, {
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
onUpdate: "RESTRICT",
|
||||
})
|
||||
user: user;
|
||||
}
|
16
src/entity/reset.ts
Normal file
16
src/entity/reset.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity()
|
||||
export class reset {
|
||||
@PrimaryColumn({ type: "varchar", length: 255 })
|
||||
mail: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
token: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
username: string;
|
||||
|
||||
@Column({ type: "varchar", length: 255 })
|
||||
secret: string;
|
||||
}
|
116
src/env.defaults.ts
Normal file
116
src/env.defaults.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
import "dotenv/config";
|
||||
import ms from "ms";
|
||||
import ip from "ip";
|
||||
|
||||
export const DB_TYPE = process.env.DB_TYPE ?? "mysql";
|
||||
export const DB_HOST = process.env.DB_HOST ?? "";
|
||||
export const DB_PORT = Number(process.env.DB_PORT ?? 3306);
|
||||
export const DB_NAME = process.env.DB_NAME ?? "";
|
||||
export const DB_USERNAME = process.env.DB_USERNAME ?? "";
|
||||
export const DB_PASSWORD = process.env.DB_PASSWORD ?? "";
|
||||
|
||||
export const SERVER_PORT = Number(process.env.SERVER_PORT ?? 5000);
|
||||
|
||||
export const JWT_SECRET = process.env.JWT_SECRET ?? "my_jwt_secret_string_ilughfnadiuhgq§$IUZGFVRweiouarbt1oub3h5q4a";
|
||||
export const JWT_EXPIRATION = process.env.JWT_EXPIRATION ?? "15m";
|
||||
export const REFRESH_EXPIRATION = process.env.REFRESH_EXPIRATION ?? "1d";
|
||||
export const PWA_REFRESH_EXPIRATION = process.env.PWA_REFRESH_EXPIRATION ?? "5d";
|
||||
|
||||
export const MAIL_USERNAME = process.env.MAIL_USERNAME ?? "";
|
||||
export const MAIL_PASSWORD = process.env.MAIL_PASSWORD ?? "";
|
||||
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 ?? "FF Admin";
|
||||
export const CLUB_WEBSITE = process.env.CLUB_WEBSITE ?? "";
|
||||
|
||||
export const BACKUP_INTERVAL = Number(process.env.BACKUP_INTERVAL ?? "1");
|
||||
export const BACKUP_COPIES = Number(process.env.BACKUP_COPIES ?? "7");
|
||||
export const BACKUP_AUTO_RESTORE = process.env.BACKUP_AUTO_RESTORE ?? "true";
|
||||
|
||||
export const USE_SECURITY_STRICT_LIMIT = process.env.USE_SECURITY_STRICT_LIMIT ?? "true";
|
||||
export const SECURITY_STRICT_LIMIT_WINDOW = process.env.SECURITY_STRICT_LIMIT_WINDOW ?? "15m";
|
||||
export const SECURITY_STRICT_LIMIT_REQUEST_COUNT = Number(process.env.SECURITY_STRICT_LIMIT_REQUEST_COUNT ?? "15");
|
||||
export const USE_SECURITY_LIMIT = process.env.USE_SECURITY_LIMIT ?? "true";
|
||||
export const SECURITY_LIMIT_WINDOW = process.env.SECURITY_LIMIT_WINDOW ?? "1m";
|
||||
export const SECURITY_LIMIT_REQUEST_COUNT = Number(process.env.SECURITY_LIMIT_REQUEST_COUNT ?? "500");
|
||||
|
||||
export const TRUST_PROXY = ((): Array<string> | string | boolean | number | null => {
|
||||
const proxyVal = process.env.TRUST_PROXY;
|
||||
if (!proxyVal) return null;
|
||||
if (proxyVal == "true" || proxyVal == "false") {
|
||||
return proxyVal == "true";
|
||||
}
|
||||
if (!isNaN(Number(proxyVal))) {
|
||||
return Number(proxyVal);
|
||||
}
|
||||
if (proxyVal.includes(",") && proxyVal.split(",").every((pv) => ip.isV4Format(pv) || ip.isV6Format(pv))) {
|
||||
return proxyVal.split(",");
|
||||
}
|
||||
if (ip.isV4Format(proxyVal) || ip.isV6Format(proxyVal)) {
|
||||
return proxyVal;
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
|
||||
export function configCheck() {
|
||||
if (DB_TYPE != "mysql" && DB_TYPE != "sqlite" && DB_TYPE != "postgres")
|
||||
throw new Error("set valid value to DB_TYPE (mysql|sqlite|postgres)");
|
||||
if ((DB_HOST == "" || typeof DB_HOST != "string") && DB_TYPE != "sqlite")
|
||||
throw new Error("set valid value to DB_HOST");
|
||||
if (DB_NAME == "" || typeof DB_NAME != "string") throw new Error("set valid value to DB_NAME");
|
||||
if ((DB_USERNAME == "" || typeof DB_USERNAME != "string") && DB_TYPE != "sqlite")
|
||||
throw new Error("set valid value to DB_USERNAME");
|
||||
if ((DB_PASSWORD == "" || typeof DB_PASSWORD != "string") && DB_TYPE != "sqlite")
|
||||
throw new Error("set valid value to DB_PASSWORD");
|
||||
|
||||
if (isNaN(SERVER_PORT)) throw new Error("set valid numeric value to SERVER_PORT");
|
||||
|
||||
if (JWT_SECRET == "" || typeof JWT_SECRET != "string") throw new Error("set valid value to JWT_SECRET");
|
||||
checkMS(JWT_EXPIRATION, "JWT_EXPIRATION");
|
||||
checkMS(REFRESH_EXPIRATION, "REFRESH_EXPIRATION");
|
||||
checkMS(PWA_REFRESH_EXPIRATION, "PWA_REFRESH_EXPIRATION");
|
||||
|
||||
if (MAIL_USERNAME == "" || typeof MAIL_USERNAME != "string") throw new Error("set valid value to MAIL_USERNAME");
|
||||
if (MAIL_PASSWORD == "" || typeof MAIL_PASSWORD != "string") throw new Error("set valid value to MAIL_PASSWORD");
|
||||
if (MAIL_HOST == "" || typeof MAIL_HOST != "string") throw new Error("set valid value to MAIL_HOST");
|
||||
if (isNaN(MAIL_PORT)) throw new Error("set valid numeric value to MAIL_PORT");
|
||||
if (MAIL_SECURE != "true" && MAIL_SECURE != "false") throw new Error("set 'true' or 'false' to MAIL_SECURE");
|
||||
|
||||
if (
|
||||
CLUB_WEBSITE != "" &&
|
||||
!/^(http(s):\/\/.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/.test(CLUB_WEBSITE)
|
||||
)
|
||||
throw new Error("CLUB_WEBSITE is not valid url");
|
||||
|
||||
if (BACKUP_AUTO_RESTORE != "true" && BACKUP_AUTO_RESTORE != "false")
|
||||
throw new Error("set 'true' or 'false' to BACKUP_AUTO_RESTORE");
|
||||
if (BACKUP_INTERVAL < 1) throw new Error("BACKUP_INTERVAL has to be at least 1");
|
||||
if (BACKUP_COPIES < 1) throw new Error("BACKUP_COPIES has to be at least 1");
|
||||
|
||||
if (USE_SECURITY_STRICT_LIMIT != "true" && USE_SECURITY_STRICT_LIMIT != "false")
|
||||
throw new Error("set 'true' or 'false' to USE_SECURITY_STRICT_LIMIT");
|
||||
checkMS(SECURITY_STRICT_LIMIT_WINDOW, "SECURITY_STRICT_LIMIT_WINDOW");
|
||||
if (isNaN(SECURITY_STRICT_LIMIT_REQUEST_COUNT))
|
||||
throw new Error("set valid numeric value to SECURITY_STRICT_LIMIT_REQUEST_COUNT");
|
||||
if (USE_SECURITY_LIMIT != "true" && USE_SECURITY_LIMIT != "false")
|
||||
throw new Error("set 'true' or 'false' to USE_SECURITY_LIMIT");
|
||||
checkMS(SECURITY_LIMIT_WINDOW, "SECURITY_LIMIT_WINDOW");
|
||||
if (isNaN(SECURITY_LIMIT_REQUEST_COUNT)) throw new Error("set valid numeric value to SECURITY_LIMIT_REQUEST_COUNT");
|
||||
|
||||
if (!TRUST_PROXY && process.env.TRUST_PROXY) {
|
||||
throw new Error("set valid boolean, number, ip or ips value to TRUST_PROXY");
|
||||
}
|
||||
}
|
||||
|
||||
function checkMS(input: string, origin: string) {
|
||||
try {
|
||||
const result = ms(input);
|
||||
if (result === undefined) {
|
||||
throw new Error(`set valid ms value to ${origin} -> [0-9]*(y|d|h|m|s)`);
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(`set valid ms value to ${origin} -> [0-9]*(y|d|h|m|s)`);
|
||||
}
|
||||
}
|
7
src/exceptions/badRequestException.ts
Normal file
7
src/exceptions/badRequestException.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import CustomRequestException from "./customRequestException";
|
||||
|
||||
export default class BadRequestException extends CustomRequestException {
|
||||
constructor(msg: string, err?: any) {
|
||||
super(400, msg, err);
|
||||
}
|
||||
}
|
12
src/exceptions/customRequestException.ts
Normal file
12
src/exceptions/customRequestException.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { ExceptionBase } from "./exceptionsBaseType";
|
||||
|
||||
export default class CustomRequestException extends Error implements ExceptionBase {
|
||||
statusCode: number;
|
||||
err?: any;
|
||||
|
||||
constructor(status: number, msg: string, err?: any) {
|
||||
super(msg);
|
||||
this.statusCode = status;
|
||||
this.err = err;
|
||||
}
|
||||
}
|
8
src/exceptions/databaseActionException.ts
Normal file
8
src/exceptions/databaseActionException.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import CustomRequestException from "./customRequestException";
|
||||
|
||||
export default class DatabaseActionException extends CustomRequestException {
|
||||
constructor(action: string, table: string, err: any) {
|
||||
let errstring = `${action} on ${table} with ${err?.code ?? "XX"} at ${err?.sqlMessage ?? "XX"}`;
|
||||
super(500, errstring, err);
|
||||
}
|
||||
}
|
3
src/exceptions/exceptionsBaseType.ts
Normal file
3
src/exceptions/exceptionsBaseType.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export type ExceptionBase = {
|
||||
statusCode: number;
|
||||
} & Error;
|
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, err?: any) {
|
||||
super(403, msg, err);
|
||||
}
|
||||
}
|
7
src/exceptions/internalException.ts
Normal file
7
src/exceptions/internalException.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import CustomRequestException from "./customRequestException";
|
||||
|
||||
export default class InternalException extends CustomRequestException {
|
||||
constructor(msg: string, err?: any) {
|
||||
super(500, msg, err);
|
||||
}
|
||||
}
|
7
src/exceptions/unauthorizedRequestException.ts
Normal file
7
src/exceptions/unauthorizedRequestException.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import CustomRequestException from "./customRequestException";
|
||||
|
||||
export default class UnauthorizedRequestException extends CustomRequestException {
|
||||
constructor(msg: string, err?: any) {
|
||||
super(401, msg, err);
|
||||
}
|
||||
}
|
27
src/factory/admin/configuration/member.ts
Normal file
27
src/factory/admin/configuration/member.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { member } from "../../../entity/configuration/member";
|
||||
import { MemberViewModel } from "../../../viewmodel/admin/configuration/member.models";
|
||||
|
||||
export default abstract class MemberFactory {
|
||||
/**
|
||||
* @description map record to member
|
||||
* @param {member} record
|
||||
* @returns {MemberViewModel}
|
||||
*/
|
||||
public static mapToSingle(record: member): MemberViewModel {
|
||||
return {
|
||||
id: record?.id,
|
||||
firstname: record?.firstname,
|
||||
lastname: record?.lastname,
|
||||
nameaffix: record?.nameaffix,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description map records to member
|
||||
* @param {Array<member>} records
|
||||
* @returns {Array<MemberViewModel>}
|
||||
*/
|
||||
public static mapToBase(records: Array<member>): Array<MemberViewModel> {
|
||||
return records.map((r) => this.mapToSingle(r));
|
||||
}
|
||||
}
|
27
src/factory/admin/management/invite.ts
Normal file
27
src/factory/admin/management/invite.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { invite } from "../../../entity/management/invite";
|
||||
import { InviteViewModel } from "../../../viewmodel/admin/management/invite.models";
|
||||
|
||||
export default abstract class InviteFactory {
|
||||
/**
|
||||
* @description map record to invite
|
||||
* @param {invite} record
|
||||
* @returns {InviteViewModel}
|
||||
*/
|
||||
public static mapToSingle(record: invite): InviteViewModel {
|
||||
return {
|
||||
mail: record.mail,
|
||||
username: record.username,
|
||||
firstname: record.firstname,
|
||||
lastname: record.lastname,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description map records to invite
|
||||
* @param {Array<invite>} records
|
||||
* @returns {Array<InviteViewModel>}
|
||||
*/
|
||||
public static mapToBase(records: Array<invite>): Array<InviteViewModel> {
|
||||
return records.map((r) => this.mapToSingle(r));
|
||||
}
|
||||
}
|
27
src/factory/admin/management/role.ts
Normal file
27
src/factory/admin/management/role.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { role } from "../../../entity/management/role";
|
||||
import PermissionHelper from "../../../helpers/permissionHelper";
|
||||
import { RoleViewModel } from "../../../viewmodel/admin/management/role.models";
|
||||
|
||||
export default abstract class RoleFactory {
|
||||
/**
|
||||
* @description map record to role
|
||||
* @param {role} record
|
||||
* @returns {roleViewModel}
|
||||
*/
|
||||
public static mapToSingle(record: role): RoleViewModel {
|
||||
return {
|
||||
id: record.id,
|
||||
permissions: PermissionHelper.convertToObject(record.permissions.map((e) => e.permission)),
|
||||
role: record.role,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description map records to role
|
||||
* @param {Array<role>} records
|
||||
* @returns {Array<roleViewModel>}
|
||||
*/
|
||||
public static mapToBase(records: Array<role>): Array<RoleViewModel> {
|
||||
return records.map((r) => this.mapToSingle(r));
|
||||
}
|
||||
}
|
39
src/factory/admin/management/user.ts
Normal file
39
src/factory/admin/management/user.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { user } from "../../../entity/management/user";
|
||||
import PermissionHelper from "../../../helpers/permissionHelper";
|
||||
import { UserViewModel } from "../../../viewmodel/admin/management/user.models";
|
||||
import RoleFactory from "./role";
|
||||
|
||||
export default abstract class UserFactory {
|
||||
/**
|
||||
* @description map record to user
|
||||
* @param {user} record
|
||||
* @returns {UserViewModel}
|
||||
*/
|
||||
public static mapToSingle(record: user): UserViewModel {
|
||||
let userPermissionStrings = record.permissions.map((e) => e.permission);
|
||||
let rolePermissions = record.roles.map((e) => e.permissions).flat();
|
||||
let rolePermissionStrings = rolePermissions.map((p) => p.permission);
|
||||
let totalPermissions = PermissionHelper.convertToObject([...userPermissionStrings, ...rolePermissionStrings]);
|
||||
|
||||
return {
|
||||
id: record.id,
|
||||
username: record.username,
|
||||
firstname: record.firstname,
|
||||
lastname: record.lastname,
|
||||
mail: record.mail,
|
||||
isOwner: record.isOwner,
|
||||
permissions: PermissionHelper.convertToObject(userPermissionStrings),
|
||||
roles: RoleFactory.mapToBase(record.roles),
|
||||
permissions_total: totalPermissions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description map records to user
|
||||
* @param {Array<role>} records
|
||||
* @returns {Array<UserViewModel>}
|
||||
*/
|
||||
public static mapToBase(records: Array<user>): Array<UserViewModel> {
|
||||
return records.map((r) => this.mapToSingle(r));
|
||||
}
|
||||
}
|
246
src/helpers/backupHelper.ts
Normal file
246
src/helpers/backupHelper.ts
Normal file
|
@ -0,0 +1,246 @@
|
|||
import { dataSource } from "../data-source";
|
||||
import { FileSystemHelper } from "./fileSystemHelper";
|
||||
import { EntityManager } from "typeorm";
|
||||
import uniqBy from "lodash.uniqby";
|
||||
import InternalException from "../exceptions/internalException";
|
||||
import UserService from "../service/management/userService";
|
||||
import { BACKUP_COPIES, BACKUP_INTERVAL } from "../env.defaults";
|
||||
import DatabaseActionException from "../exceptions/databaseActionException";
|
||||
|
||||
export type BackupSection = "member" | "user";
|
||||
|
||||
export type BackupSectionRefered = {
|
||||
[key in BackupSection]?: Array<string>;
|
||||
};
|
||||
|
||||
export type BackupFileContent = { [key in BackupSection]?: BackupFileContentSection } & {
|
||||
backup_file_details: { collectIds: boolean; createdAt: Date; version: 1 };
|
||||
};
|
||||
export type BackupFileContentSection = Array<any> | { [key: string]: Array<any> };
|
||||
|
||||
export default abstract class BackupHelper {
|
||||
// ! Order matters because of foreign keys
|
||||
private static readonly backupSection: Array<{ type: BackupSection; orderOnInsert: number; orderOnClear: number }> = [
|
||||
{ type: "member", orderOnInsert: 2, orderOnClear: 2 },
|
||||
{ type: "user", orderOnInsert: 1, orderOnClear: 1 },
|
||||
];
|
||||
|
||||
private static readonly backupSectionRefered: BackupSectionRefered = {
|
||||
member: ["member"],
|
||||
user: ["user", "user_permission", "role", "role_permission", "invite"],
|
||||
};
|
||||
|
||||
private static transactionManager: EntityManager;
|
||||
|
||||
static async createBackup({
|
||||
filename,
|
||||
path = "/backup",
|
||||
collectIds = true,
|
||||
}: {
|
||||
filename?: string;
|
||||
path?: string;
|
||||
collectIds?: boolean;
|
||||
}): Promise<void> {
|
||||
if (!filename) {
|
||||
filename = new Date().toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
let json: BackupFileContent = { backup_file_details: { collectIds, createdAt: new Date(), version: 1 } };
|
||||
for (const section of this.backupSection) {
|
||||
json[section.type] = await this.getSectionData(section.type, collectIds);
|
||||
}
|
||||
|
||||
FileSystemHelper.writeFile(path, filename + ".json", JSON.stringify(json, null, 2));
|
||||
|
||||
let files = FileSystemHelper.getFilesInDirectory("backup", ".json");
|
||||
let sorted = files.sort((a, b) => new Date(b.split(".")[0]).getTime() - new Date(a.split(".")[0]).getTime());
|
||||
|
||||
const filesToDelete = sorted.slice(BACKUP_COPIES);
|
||||
for (const file of filesToDelete) {
|
||||
FileSystemHelper.deleteFile("backup", file);
|
||||
}
|
||||
}
|
||||
|
||||
static async createBackupOnInterval() {
|
||||
let files = FileSystemHelper.getFilesInDirectory("backup", ".json");
|
||||
let newestFile = files.sort((a, b) => new Date(b.split(".")[0]).getTime() - new Date(a.split(".")[0]).getTime())[0];
|
||||
|
||||
let lastBackup = new Date(newestFile.split(".")[0]);
|
||||
let diffInMs = new Date().getTime() - lastBackup.getTime();
|
||||
let diffInDays = diffInMs / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (diffInDays >= BACKUP_INTERVAL) {
|
||||
await this.createBackup({});
|
||||
}
|
||||
}
|
||||
|
||||
static async loadBackup({
|
||||
filename,
|
||||
path = "/backup",
|
||||
include = [],
|
||||
partial = false,
|
||||
overwrite = false,
|
||||
}: {
|
||||
filename: string;
|
||||
path?: string;
|
||||
partial?: boolean;
|
||||
include?: Array<BackupSection>;
|
||||
overwrite?: boolean;
|
||||
}): Promise<void> {
|
||||
this.transactionManager = undefined;
|
||||
|
||||
let file = FileSystemHelper.readFile(`${path}/${filename}`);
|
||||
let backup: BackupFileContent = JSON.parse(file);
|
||||
|
||||
if ((partial && include.length == 0) || (!partial && include.length != 0)) {
|
||||
throw new InternalException("partial and include have to be set correctly for restoring backup.");
|
||||
}
|
||||
|
||||
await dataSource.manager
|
||||
.transaction(async (transaction) => {
|
||||
this.transactionManager = transaction;
|
||||
|
||||
const sections = this.backupSection
|
||||
.filter((bs) => (partial ? include.includes(bs.type) : true))
|
||||
.sort((a, b) => a.orderOnClear - b.orderOnClear);
|
||||
if (!overwrite) {
|
||||
for (const section of sections.filter((s) => Object.keys(backup).includes(s.type))) {
|
||||
let refered = this.backupSectionRefered[section.type];
|
||||
for (const ref of refered) {
|
||||
await this.transactionManager.getRepository(ref).delete({});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const section of sections
|
||||
.filter((s) => Object.keys(backup).includes(s.type))
|
||||
.sort((a, b) => a.orderOnInsert - b.orderOnInsert)) {
|
||||
await this.setSectionData(section.type, backup[section.type], backup.backup_file_details.collectIds ?? false);
|
||||
}
|
||||
|
||||
this.transactionManager = undefined;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
this.transactionManager = undefined;
|
||||
throw new DatabaseActionException("BACKUP RESTORE", include.join(", ") || "FULL", err);
|
||||
});
|
||||
}
|
||||
|
||||
public static async autoRestoreBackup() {
|
||||
let count = await UserService.count();
|
||||
if (count == 0) {
|
||||
let files = FileSystemHelper.getFilesInDirectory("/backup", ".json");
|
||||
let newestFile = files.sort(
|
||||
(a, b) => new Date(b.split(".")[0]).getTime() - new Date(a.split(".")[0]).getTime()
|
||||
)[0];
|
||||
|
||||
if (newestFile) {
|
||||
console.log(`${new Date().toISOString()}: auto-restoring ${newestFile}`);
|
||||
await this.loadBackup({ filename: newestFile });
|
||||
console.log(`${new Date().toISOString()}: finished auto-restore`);
|
||||
} else {
|
||||
console.log(`${new Date().toISOString()}: skip auto-restore as no backup was found`);
|
||||
}
|
||||
} else {
|
||||
console.log(`${new Date().toISOString()}: skip auto-restore as users exist`);
|
||||
}
|
||||
}
|
||||
|
||||
private static async getSectionData(
|
||||
section: BackupSection,
|
||||
collectIds: boolean
|
||||
): Promise<Array<any> | { [key: string]: any }> {
|
||||
switch (section) {
|
||||
case "member":
|
||||
return await this.getMemberData(collectIds);
|
||||
case "user":
|
||||
return await this.getUser(collectIds);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static async getMemberData(collectIds: boolean): Promise<Array<any>> {
|
||||
return await dataSource
|
||||
.getRepository("member")
|
||||
.createQueryBuilder("member")
|
||||
.select([...(collectIds ? ["member.id"] : []), "member.firstname", "member.lastname", "member.nameaffix"])
|
||||
.getMany();
|
||||
}
|
||||
private static async getUser(collectIds: boolean): Promise<{ [key: string]: Array<any> }> {
|
||||
return {
|
||||
user: await dataSource
|
||||
.getRepository("user")
|
||||
.createQueryBuilder("user")
|
||||
.leftJoin("user.roles", "roles")
|
||||
.leftJoin("roles.permissions", "role_permissions")
|
||||
.leftJoin("user.permissions", "permissions")
|
||||
.select([
|
||||
...(collectIds ? ["user.id"] : []),
|
||||
"user.mail",
|
||||
"user.username",
|
||||
"user.firstname",
|
||||
"user.lastname",
|
||||
"user.secret",
|
||||
"user.isOwner",
|
||||
])
|
||||
.addSelect(["permissions.permission"])
|
||||
.addSelect(["roles.role"])
|
||||
.addSelect(["role_permissions.permission"])
|
||||
.getMany(),
|
||||
role: await dataSource
|
||||
.getRepository("role")
|
||||
.createQueryBuilder("role")
|
||||
.leftJoin("role.permissions", "permissions")
|
||||
.addSelect(["role.role"])
|
||||
.addSelect(["permissions.permission"])
|
||||
.getMany(),
|
||||
invite: await dataSource.getRepository("invite").find(),
|
||||
};
|
||||
}
|
||||
|
||||
private static async setSectionData(
|
||||
section: BackupSection,
|
||||
data: BackupFileContentSection,
|
||||
collectedIds: boolean
|
||||
): Promise<void> {
|
||||
if (section == "member" && Array.isArray(data)) await this.setMemberData(data);
|
||||
if (section == "user" && !Array.isArray(data)) await this.setUser(data);
|
||||
}
|
||||
|
||||
private static async setMemberData(data: Array<any>): Promise<void> {
|
||||
await this.transactionManager.getRepository("member").save(data);
|
||||
}
|
||||
private static async setUser(data: { [key: string]: Array<any> }): Promise<void> {
|
||||
let usedRoles = (data?.["user"] ?? [])
|
||||
.map((d) => d.roles)
|
||||
.flat()
|
||||
.map((d) => ({ ...d, id: undefined }));
|
||||
|
||||
await this.transactionManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into("role")
|
||||
.values(uniqBy([...(data?.["role"] ?? []), ...usedRoles], "role"))
|
||||
.orIgnore()
|
||||
.execute();
|
||||
|
||||
let roles = await this.transactionManager.getRepository("role").find();
|
||||
let dataWithMappedIds = (data?.["user"] ?? []).map((u) => ({
|
||||
...u,
|
||||
roles: u.roles.map((r: any) => ({
|
||||
...r,
|
||||
id: roles.find((role) => role.role == r.role)?.id ?? undefined,
|
||||
})),
|
||||
}));
|
||||
await this.transactionManager.getRepository("user").save(dataWithMappedIds);
|
||||
await this.transactionManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into("invite")
|
||||
.values(data["invite"])
|
||||
.orIgnore()
|
||||
.execute();
|
||||
}
|
||||
}
|
71
src/helpers/fileSystemHelper.ts
Normal file
71
src/helpers/fileSystemHelper.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { readdirSync } from "fs";
|
||||
|
||||
export abstract class FileSystemHelper {
|
||||
static createFolder(...args: string[]) {
|
||||
const exportPath = this.formatPath(...args);
|
||||
if (!existsSync(exportPath)) {
|
||||
mkdirSync(exportPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
static readFile(...filePath: string[]) {
|
||||
this.createFolder(...filePath);
|
||||
return readFileSync(this.formatPath(...filePath), "utf8");
|
||||
}
|
||||
|
||||
static readFileasBase64(...filePath: string[]) {
|
||||
this.createFolder(...filePath);
|
||||
return readFileSync(this.formatPath(...filePath), "base64");
|
||||
}
|
||||
|
||||
static readTemplateFile(filePath: string) {
|
||||
this.createFolder(filePath);
|
||||
return readFileSync(process.cwd() + filePath, "utf8");
|
||||
}
|
||||
|
||||
static writeFile(filePath: string, filename: string, file: any) {
|
||||
this.createFolder(filePath);
|
||||
let path = this.formatPath(filePath, filename);
|
||||
writeFileSync(path, file);
|
||||
}
|
||||
|
||||
static deleteFile(...filePath: string[]) {
|
||||
const path = this.formatPath(...filePath);
|
||||
if (existsSync(path)) {
|
||||
unlinkSync(path);
|
||||
}
|
||||
}
|
||||
|
||||
static formatPath(...args: string[]) {
|
||||
return join(process.cwd(), "files", ...args);
|
||||
}
|
||||
|
||||
static normalizePath(...args: string[]) {
|
||||
return join(...args);
|
||||
}
|
||||
|
||||
static getFilesInDirectory(directoryPath: string, filetype?: string): string[] {
|
||||
const fullPath = this.formatPath(directoryPath);
|
||||
if (!existsSync(fullPath)) {
|
||||
return [];
|
||||
}
|
||||
return readdirSync(fullPath, { withFileTypes: true })
|
||||
.filter((dirent) => !dirent.isDirectory() && (!filetype || dirent.name.endsWith(filetype)))
|
||||
.map((dirent) => dirent.name);
|
||||
}
|
||||
|
||||
static clearDirectoryByFiletype(directoryPath: string, filetype: string) {
|
||||
const fullPath = this.formatPath(directoryPath);
|
||||
if (!existsSync(fullPath)) {
|
||||
return;
|
||||
}
|
||||
readdirSync(fullPath, { withFileTypes: true })
|
||||
.filter((dirent) => !dirent.isDirectory() && dirent.name.endsWith(filetype))
|
||||
.forEach((dirent) => {
|
||||
const filePath = join(fullPath, dirent.name);
|
||||
unlinkSync(filePath);
|
||||
});
|
||||
}
|
||||
}
|
78
src/helpers/jwtHelper.ts
Normal file
78
src/helpers/jwtHelper.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
import jwt from "jsonwebtoken";
|
||||
import { JWTData, JWTToken } from "../type/jwtTypes";
|
||||
import { JWT_SECRET, JWT_EXPIRATION } from "../env.defaults";
|
||||
import InternalException from "../exceptions/internalException";
|
||||
import RolePermissionService from "../service/management/rolePermissionService";
|
||||
import UserPermissionService from "../service/management/userPermissionService";
|
||||
import UserService from "../service/management/userService";
|
||||
import PermissionHelper from "./permissionHelper";
|
||||
|
||||
export abstract class JWTHelper {
|
||||
static validate(token: string): Promise<string | jwt.JwtPayload> {
|
||||
return new Promise<string | jwt.JwtPayload>((resolve, reject) => {
|
||||
jwt.verify(token, JWT_SECRET, (err, decoded) => {
|
||||
if (err) reject(err.message);
|
||||
else resolve(decoded);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
{
|
||||
...(useExpiration ?? true ? { expiresIn: expOverwrite ?? JWT_EXPIRATION } : {}),
|
||||
},
|
||||
(err, token) => {
|
||||
if (err) reject(err.message);
|
||||
else resolve(token);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static decode(token: string): Promise<string | jwt.JwtPayload> {
|
||||
return new Promise<string | jwt.JwtPayload>((resolve, reject) => {
|
||||
try {
|
||||
let decoded = jwt.decode(token);
|
||||
resolve(decoded);
|
||||
} catch (err) {
|
||||
reject(err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async buildToken(id: string): Promise<string> {
|
||||
let { firstname, lastname, mail, username, isOwner } = await UserService.getById(id);
|
||||
let userPermissions = await UserPermissionService.getByUser(id);
|
||||
let userPermissionStrings = userPermissions.map((e) => e.permission);
|
||||
let userRoles = await UserService.getAssignedRolesByUserId(id);
|
||||
let rolePermissions =
|
||||
userRoles.length != 0 ? await RolePermissionService.getByRoles(userRoles.map((e) => e.id)) : [];
|
||||
let rolePermissionStrings = rolePermissions.map((e) => e.permission);
|
||||
let permissionObject = PermissionHelper.convertToObject([...userPermissionStrings, ...rolePermissionStrings]);
|
||||
|
||||
let jwtData: JWTToken = {
|
||||
userId: id,
|
||||
mail: mail,
|
||||
username: username,
|
||||
firstname: firstname,
|
||||
lastname: lastname,
|
||||
isOwner: isOwner,
|
||||
permissions: permissionObject,
|
||||
};
|
||||
|
||||
return await JWTHelper.create(jwtData)
|
||||
.then((result) => {
|
||||
return result;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException("Failed accessToken creation", err);
|
||||
});
|
||||
}
|
||||
}
|
43
src/helpers/mailHelper.ts
Normal file
43
src/helpers/mailHelper.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { Transporter, createTransport, TransportOptions } from "nodemailer";
|
||||
import { CLUB_NAME, MAIL_HOST, MAIL_PASSWORD, MAIL_PORT, MAIL_SECURE, MAIL_USERNAME } from "../env.defaults";
|
||||
import { Attachment } from "nodemailer/lib/mailer";
|
||||
|
||||
export default abstract class MailHelper {
|
||||
private static readonly transporter: Transporter = createTransport({
|
||||
host: MAIL_HOST,
|
||||
port: MAIL_PORT,
|
||||
secure: (MAIL_SECURE as "true" | "false") == "true",
|
||||
auth: {
|
||||
user: MAIL_USERNAME,
|
||||
pass: MAIL_PASSWORD,
|
||||
},
|
||||
} as TransportOptions);
|
||||
|
||||
/**
|
||||
* @description send mail
|
||||
* @param {string} target
|
||||
* @param {string} subject
|
||||
* @param {string} content
|
||||
* @returns {Prmose<*>}
|
||||
*/
|
||||
static async sendMail(
|
||||
target: string,
|
||||
subject: string,
|
||||
content: string,
|
||||
attach: Array<Attachment> = []
|
||||
): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.transporter
|
||||
.sendMail({
|
||||
from: `"${CLUB_NAME}" <${MAIL_USERNAME}>`,
|
||||
to: target,
|
||||
subject,
|
||||
text: content,
|
||||
html: content,
|
||||
attachments: attach,
|
||||
})
|
||||
.then((info) => resolve(info.messageId))
|
||||
.catch((e) => reject(e));
|
||||
});
|
||||
}
|
||||
}
|
28
src/helpers/parameterPassCheckHelper.ts
Normal file
28
src/helpers/parameterPassCheckHelper.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { Request, Response } from "express";
|
||||
import BadRequestException from "../exceptions/badRequestException";
|
||||
|
||||
export default class ParamaterPassCheckHelper {
|
||||
static requiredIncluded(testfor: Array<string>, obj: object) {
|
||||
let result = testfor.every((key) => Object.keys(obj).includes(key));
|
||||
if (!result) throw new BadRequestException(`not all required parameters included: ${testfor.join(",")}`);
|
||||
}
|
||||
|
||||
static forbiddenIncluded(testfor: Array<string>, obj: object) {
|
||||
let result = testfor.some((key) => Object.keys(obj).includes(key));
|
||||
if (!result) throw new BadRequestException(`PPC: forbidden parameters included: ${testfor.join(",")}`);
|
||||
}
|
||||
|
||||
static requiredIncludedMiddleware(testfor: Array<string>): (req: Request, res: Response, next: Function) => void {
|
||||
return (req: Request, res: Response, next: Function) => {
|
||||
this.requiredIncluded(testfor, req.body);
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
static forbiddenIncludedMiddleware(testfor: Array<string>): (req: Request, res: Response, next: Function) => void {
|
||||
return (req: Request, res: Response, next: Function) => {
|
||||
this.requiredIncluded(testfor, req.body);
|
||||
next();
|
||||
};
|
||||
}
|
||||
}
|
255
src/helpers/permissionHelper.ts
Normal file
255
src/helpers/permissionHelper.ts
Normal file
|
@ -0,0 +1,255 @@
|
|||
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 can(
|
||||
permissions: PermissionObject,
|
||||
type: PermissionType | "admin",
|
||||
section: PermissionSection,
|
||||
module?: PermissionModule
|
||||
) {
|
||||
if (type == "admin") return permissions?.admin ?? false;
|
||||
if (permissions?.admin) return true;
|
||||
if (
|
||||
(!module &&
|
||||
permissions[section] != undefined &&
|
||||
(permissions[section]?.all == "*" || permissions[section]?.all?.includes(type))) ||
|
||||
permissions[section]?.all == "*" ||
|
||||
permissions[section]?.all?.includes(type)
|
||||
)
|
||||
return true;
|
||||
if (module && (permissions[section]?.[module] == "*" || permissions[section]?.[module]?.includes(type)))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static canSome(
|
||||
permissions: PermissionObject,
|
||||
checks: Array<{
|
||||
requiredPermissions: PermissionType | "admin";
|
||||
section: PermissionSection;
|
||||
module?: PermissionModule;
|
||||
}>
|
||||
) {
|
||||
checks.reduce<boolean>((prev, curr) => {
|
||||
return prev || this.can(permissions, curr.requiredPermissions, curr.section, curr.module);
|
||||
}, false);
|
||||
}
|
||||
|
||||
static canSection(
|
||||
permissions: PermissionObject,
|
||||
type: PermissionType | "admin",
|
||||
section: PermissionSection
|
||||
): boolean {
|
||||
if (type == "admin") return permissions?.admin ?? false;
|
||||
if (permissions?.admin) return true;
|
||||
if (
|
||||
permissions[section]?.all == "*" ||
|
||||
permissions[section]?.all?.includes(type) ||
|
||||
permissions[section] != undefined
|
||||
)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static canSomeSection(
|
||||
permissions: PermissionObject,
|
||||
checks: Array<{
|
||||
requiredPermissions: PermissionType | "admin";
|
||||
section: PermissionSection;
|
||||
}>
|
||||
): boolean {
|
||||
return checks.reduce<boolean>((prev, curr) => {
|
||||
return prev || this.can(permissions, curr.requiredPermissions, curr.section);
|
||||
}, false);
|
||||
}
|
||||
|
||||
static passCheckMiddleware(
|
||||
requiredPermissions: PermissionType | "admin",
|
||||
section: PermissionSection,
|
||||
module?: PermissionModule
|
||||
): (req: Request, res: Response, next: Function) => void {
|
||||
return (req: Request, res: Response, next: Function) => {
|
||||
const permissions = req.permissions;
|
||||
const isOwner = req.isOwner;
|
||||
|
||||
if (isOwner || this.can(permissions, requiredPermissions, section, module)) {
|
||||
next();
|
||||
} else {
|
||||
throw new ForbiddenRequestException(`missing permission for ${section}.${module}.${requiredPermissions}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static passCheckSomeMiddleware(
|
||||
checks: Array<{
|
||||
requiredPermissions: PermissionType | "admin";
|
||||
section: PermissionSection;
|
||||
module?: PermissionModule;
|
||||
}>
|
||||
): (req: Request, res: Response, next: Function) => void {
|
||||
return (req: Request, res: Response, next: Function) => {
|
||||
const permissions = req.permissions;
|
||||
const isOwner = req.isOwner;
|
||||
|
||||
if (isOwner || this.canSome(permissions, checks)) {
|
||||
next();
|
||||
} else {
|
||||
let permissionsToPass = checks.reduce<string>((prev, curr) => {
|
||||
return prev + (prev != " or " ? "" : "") + `${curr.section}.${curr.module}.${curr.requiredPermissions}`;
|
||||
}, "");
|
||||
throw new ForbiddenRequestException(`missing permission for ${permissionsToPass}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static sectionPassCheckMiddleware(
|
||||
requiredPermissions: PermissionType | "admin",
|
||||
section: PermissionSection
|
||||
): (req: Request, res: Response, next: Function) => void {
|
||||
return (req: Request, res: Response, next: Function) => {
|
||||
const permissions = req.permissions;
|
||||
const isOwner = req.isOwner;
|
||||
|
||||
if (isOwner || this.canSection(permissions, requiredPermissions, section)) {
|
||||
next();
|
||||
} else {
|
||||
throw new ForbiddenRequestException(`missing permission for ${section}.${module}.${requiredPermissions}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static sectionPassCheckSomeMiddleware(
|
||||
checks: Array<{ requiredPermissions: PermissionType | "admin"; section: PermissionSection }>
|
||||
): (req: Request, res: Response, next: Function) => void {
|
||||
return (req: Request, res: Response, next: Function) => {
|
||||
const permissions = req.permissions;
|
||||
const isOwner = req.isOwner;
|
||||
|
||||
if (isOwner || this.canSomeSection(permissions, checks)) {
|
||||
next();
|
||||
} else {
|
||||
let permissionsToPass = checks.reduce<string>((prev, curr) => {
|
||||
return prev + (prev != " or " ? "" : "") + `${curr.section}.${curr.requiredPermissions}`;
|
||||
}, "");
|
||||
throw new ForbiddenRequestException(`missing permission for ${permissionsToPass}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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<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);
|
||||
|
||||
if (output[split[0]].all != "*") {
|
||||
output[split[0]].all = [
|
||||
...new Set([...output[split[0]].all, ...appliedPermissions]),
|
||||
] as Array<PermissionType>;
|
||||
}
|
||||
} 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;
|
||||
if (output[split[0]][split[1] as PermissionModule] != "*") {
|
||||
output[split[0]][split[1] as PermissionModule] = [
|
||||
...new Set([...output[split[0]][split[1] as PermissionModule], ...appliedPermissions]),
|
||||
] as Array<PermissionType>;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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 == "*" || types.length == permissionTypes.length) {
|
||||
output.push(`${section}.*`);
|
||||
} else {
|
||||
for (let type of types) {
|
||||
output.push(`${section}.${type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
let modules = Object.keys(permissions[section]).filter((m: PermissionModule) =>
|
||||
permissionModules.includes(m)
|
||||
) as Array<PermissionModule>;
|
||||
for (let module of modules) {
|
||||
let types = permissions[section][module];
|
||||
if (types == "*" || types.length == permissionTypes.length) {
|
||||
output.push(`${section}.${module}.*`);
|
||||
} else {
|
||||
for (let type of types) {
|
||||
output.push(`${section}.${module}.${type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
static getWhatToAdd(before: Array<PermissionString>, after: Array<PermissionString>): Array<PermissionString> {
|
||||
return after.filter((permission) => !before.includes(permission));
|
||||
}
|
||||
|
||||
static getWhatToRemove(before: Array<PermissionString>, after: Array<PermissionString>): Array<PermissionString> {
|
||||
return before.filter((permission) => !after.includes(permission));
|
||||
}
|
||||
}
|
17
src/helpers/stringHelper.ts
Normal file
17
src/helpers/stringHelper.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import crypto from "crypto";
|
||||
|
||||
export abstract class StringHelper {
|
||||
static random(len: number, charSet?: string): string {
|
||||
// charSet = charSet || "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
// var randomString = "";
|
||||
// for (var i = 0; i < len; i++) {
|
||||
// var randomPoz = Math.floor(Math.random() * charSet.length);
|
||||
// randomString += charSet.substring(randomPoz, randomPoz + 1);
|
||||
// }
|
||||
// return randomString;
|
||||
return crypto
|
||||
.randomBytes(len)
|
||||
.toString("base64")
|
||||
.replace(/[^a-zA-Z0-9]/g, "");
|
||||
}
|
||||
}
|
44
src/index.ts
Normal file
44
src/index.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import "dotenv/config";
|
||||
import express from "express";
|
||||
|
||||
import { BACKUP_AUTO_RESTORE, configCheck, SERVER_PORT } from "./env.defaults";
|
||||
configCheck();
|
||||
|
||||
import { PermissionObject } from "./type/permissionTypes";
|
||||
declare global {
|
||||
namespace Express {
|
||||
export interface Request {
|
||||
userId: string;
|
||||
username: string;
|
||||
isOwner: boolean;
|
||||
permissions: PermissionObject;
|
||||
isPWA: boolean;
|
||||
isWebApiRequest: boolean;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import { dataSource } from "./data-source";
|
||||
import BackupHelper from "./helpers/backupHelper";
|
||||
dataSource.initialize().then(async () => {
|
||||
if ((BACKUP_AUTO_RESTORE as "true" | "false") == "true" && (await dataSource.createQueryRunner().hasTable("user"))) {
|
||||
await BackupHelper.autoRestoreBackup().catch((err) => {
|
||||
console.log(`${new Date().toISOString()}: failed auto-restoring database`, err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const app = express();
|
||||
import router from "./routes/index";
|
||||
router(app);
|
||||
app.listen(process.env.NODE_ENV ? SERVER_PORT : 5000, () => {
|
||||
console.log(`${new Date().toISOString()}: listening on port ${process.env.NODE_ENV ? SERVER_PORT : 5000}`);
|
||||
});
|
||||
|
||||
import schedule from "node-schedule";
|
||||
import RefreshCommandHandler from "./command/refreshCommandHandler";
|
||||
const job = schedule.scheduleJob("0 0 * * *", async () => {
|
||||
console.log(`${new Date().toISOString()}: running Cron`);
|
||||
await RefreshCommandHandler.deleteExpired();
|
||||
await BackupHelper.createBackupOnInterval();
|
||||
});
|
12
src/middleware/allowSetup.ts
Normal file
12
src/middleware/allowSetup.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { NextFunction, Request, Response } from "express";
|
||||
import UserService from "../service/management/userService";
|
||||
import CustomRequestException from "../exceptions/customRequestException";
|
||||
|
||||
export default async function allowSetup(req: Request, res: Response, next: NextFunction) {
|
||||
let count = await UserService.count();
|
||||
if (count != 0) {
|
||||
throw new CustomRequestException(405, "service is already set up");
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
43
src/middleware/authenticate.ts
Normal file
43
src/middleware/authenticate.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { NextFunction, 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 authenticate(req: Request, res: Response, next: NextFunction) {
|
||||
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 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();
|
||||
}
|
11
src/middleware/detectPWA.ts
Normal file
11
src/middleware/detectPWA.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { NextFunction, Request, Response } from "express";
|
||||
|
||||
export default async function detectPWA(req: Request, res: Response, next: NextFunction) {
|
||||
const userAgent = req.headers["user-agent"] || "";
|
||||
if ((userAgent.includes("Mobile") && userAgent.includes("Standalone")) || req.headers["x-pwa-client"] === "true") {
|
||||
req.isPWA = true;
|
||||
} else {
|
||||
req.isPWA = false;
|
||||
}
|
||||
next();
|
||||
}
|
22
src/middleware/errorHandler.ts
Normal file
22
src/middleware/errorHandler.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { NextFunction, Request, Response } from "express";
|
||||
import { ExceptionBase } from "../exceptions/exceptionsBaseType";
|
||||
import CustomRequestException from "../exceptions/customRequestException";
|
||||
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
|
||||
|
||||
export default function errorHandler(err: ExceptionBase | Error, req: Request, res: Response, next: NextFunction) {
|
||||
let status = 500;
|
||||
let msg = "Internal Server Error";
|
||||
|
||||
if (err instanceof CustomRequestException) {
|
||||
status = err.statusCode;
|
||||
msg = err.message;
|
||||
}
|
||||
|
||||
if (err instanceof CustomRequestException) {
|
||||
console.log("Custom Handler", status, msg);
|
||||
} else {
|
||||
console.log("Error Handler", err);
|
||||
}
|
||||
|
||||
res.status(status).send(msg);
|
||||
}
|
42
src/migrations/1739697068682-CreateSchema.ts
Normal file
42
src/migrations/1739697068682-CreateSchema.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
import {
|
||||
invite_table,
|
||||
refresh_table,
|
||||
reset_table,
|
||||
role_permission_table,
|
||||
role_table,
|
||||
user_permission_table,
|
||||
user_roles_table,
|
||||
user_table,
|
||||
} from "./baseSchemaTables/admin";
|
||||
import { member_table } from "./baseSchemaTables/member";
|
||||
|
||||
export class CreateSchema1739697068682 implements MigrationInterface {
|
||||
name = "CreateSchema1739697068682";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createTable(reset_table, true, true, true);
|
||||
await queryRunner.createTable(invite_table, true, true, true);
|
||||
await queryRunner.createTable(role_table, true, true, true);
|
||||
await queryRunner.createTable(role_permission_table, true, true, true);
|
||||
await queryRunner.createTable(user_table, true, true, true);
|
||||
await queryRunner.createTable(user_roles_table, true, true, true);
|
||||
await queryRunner.createTable(user_permission_table, true, true, true);
|
||||
await queryRunner.createTable(refresh_table, true, true, true);
|
||||
|
||||
await queryRunner.createTable(member_table, true, true, true);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropTable("member", true, true, true);
|
||||
|
||||
await queryRunner.dropTable("refresh", true, true, true);
|
||||
await queryRunner.dropTable("user_permission", true, true, true);
|
||||
await queryRunner.dropTable("user_roles", true, true, true);
|
||||
await queryRunner.dropTable("user", true, true, true);
|
||||
await queryRunner.dropTable("role_permission", true, true, true);
|
||||
await queryRunner.dropTable("role", true, true, true);
|
||||
await queryRunner.dropTable("invite", true, true, true);
|
||||
await queryRunner.dropTable("reset", true, true, true);
|
||||
}
|
||||
}
|
121
src/migrations/baseSchemaTables/admin.ts
Normal file
121
src/migrations/baseSchemaTables/admin.ts
Normal file
|
@ -0,0 +1,121 @@
|
|||
import { Table, TableForeignKey } from "typeorm";
|
||||
import { getDefaultByORM, getTypeByORM, isIncrementPrimary, isUUIDPrimary } from "../ormHelper";
|
||||
|
||||
export const invite_table = new Table({
|
||||
name: "invite",
|
||||
columns: [
|
||||
{ name: "mail", ...getTypeByORM("varchar"), isPrimary: true },
|
||||
{ name: "token", ...getTypeByORM("varchar") },
|
||||
{ name: "username", ...getTypeByORM("varchar") },
|
||||
{ name: "firstname", ...getTypeByORM("varchar") },
|
||||
{ name: "lastname", ...getTypeByORM("varchar") },
|
||||
{ name: "secret", ...getTypeByORM("varchar") },
|
||||
],
|
||||
});
|
||||
export const role_table = new Table({
|
||||
name: "role",
|
||||
columns: [
|
||||
{ name: "id", ...getTypeByORM("int"), ...isIncrementPrimary },
|
||||
{ name: "role", ...getTypeByORM("varchar"), isUnique: true },
|
||||
],
|
||||
});
|
||||
|
||||
export const role_permission_table = new Table({
|
||||
name: "role_permission",
|
||||
columns: [
|
||||
{ name: "roleId", ...getTypeByORM("int"), isPrimary: true },
|
||||
{ name: "permission", ...getTypeByORM("varchar"), isPrimary: true },
|
||||
],
|
||||
foreignKeys: [
|
||||
new TableForeignKey({
|
||||
columnNames: ["roleId"],
|
||||
referencedColumnNames: ["id"],
|
||||
referencedTableName: "role",
|
||||
onDelete: "CASCADE",
|
||||
onUpdate: "RESTRICT",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export const user_table = new Table({
|
||||
name: "user",
|
||||
columns: [
|
||||
{ name: "id", ...getTypeByORM("uuid"), ...isUUIDPrimary },
|
||||
{ name: "mail", ...getTypeByORM("varchar"), isUnique: true },
|
||||
{ name: "username", ...getTypeByORM("varchar"), isUnique: true },
|
||||
{ name: "firstname", ...getTypeByORM("varchar") },
|
||||
{ name: "lastname", ...getTypeByORM("varchar") },
|
||||
{ name: "secret", ...getTypeByORM("varchar") },
|
||||
{ name: "static", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) },
|
||||
{ name: "isOwner", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) },
|
||||
],
|
||||
});
|
||||
|
||||
export const user_roles_table = new Table({
|
||||
name: "user_roles",
|
||||
columns: [
|
||||
{ name: "userId", ...getTypeByORM("uuid"), isPrimary: true },
|
||||
{ name: "roleId", ...getTypeByORM("int"), isPrimary: true },
|
||||
],
|
||||
foreignKeys: [
|
||||
new TableForeignKey({
|
||||
columnNames: ["userId"],
|
||||
referencedColumnNames: ["id"],
|
||||
referencedTableName: "user",
|
||||
onDelete: "CASCADE",
|
||||
onUpdate: "RESTRICT",
|
||||
}),
|
||||
new TableForeignKey({
|
||||
columnNames: ["roleId"],
|
||||
referencedColumnNames: ["id"],
|
||||
referencedTableName: "role",
|
||||
onDelete: "CASCADE",
|
||||
onUpdate: "RESTRICT",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export const user_permission_table = new Table({
|
||||
name: "user_permission",
|
||||
columns: [
|
||||
{ name: "userId", ...getTypeByORM("uuid"), isPrimary: true },
|
||||
{ name: "permission", ...getTypeByORM("varchar"), isPrimary: true },
|
||||
],
|
||||
foreignKeys: [
|
||||
new TableForeignKey({
|
||||
columnNames: ["userId"],
|
||||
referencedColumnNames: ["id"],
|
||||
referencedTableName: "user",
|
||||
onDelete: "CASCADE",
|
||||
onUpdate: "RESTRICT",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export const refresh_table = new Table({
|
||||
name: "refresh",
|
||||
columns: [
|
||||
{ name: "token", ...getTypeByORM("varchar"), isPrimary: true },
|
||||
{ name: "expiry", ...getTypeByORM("datetime", false, 6) },
|
||||
{ name: "userId", ...getTypeByORM("uuid"), isPrimary: true },
|
||||
],
|
||||
foreignKeys: [
|
||||
new TableForeignKey({
|
||||
columnNames: ["userId"],
|
||||
referencedColumnNames: ["id"],
|
||||
referencedTableName: "user",
|
||||
onDelete: "CASCADE",
|
||||
onUpdate: "RESTRICT",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export const reset_table = new Table({
|
||||
name: "reset",
|
||||
columns: [
|
||||
{ name: "mail", ...getTypeByORM("varchar"), isPrimary: true },
|
||||
{ name: "token", ...getTypeByORM("varchar") },
|
||||
{ name: "username", ...getTypeByORM("varchar") },
|
||||
{ name: "secret", ...getTypeByORM("varchar") },
|
||||
],
|
||||
});
|
12
src/migrations/baseSchemaTables/member.ts
Normal file
12
src/migrations/baseSchemaTables/member.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { Table } from "typeorm";
|
||||
import { getTypeByORM, isUUIDPrimary } from "../ormHelper";
|
||||
|
||||
export const member_table = new Table({
|
||||
name: "member",
|
||||
columns: [
|
||||
{ name: "id", ...getTypeByORM("uuid"), ...isUUIDPrimary },
|
||||
{ name: "firstname", ...getTypeByORM("varchar") },
|
||||
{ name: "lastname", ...getTypeByORM("varchar") },
|
||||
{ name: "nameaffix", ...getTypeByORM("varchar") },
|
||||
],
|
||||
});
|
105
src/migrations/ormHelper.ts
Normal file
105
src/migrations/ormHelper.ts
Normal file
|
@ -0,0 +1,105 @@
|
|||
export type ORMType = "int" | "bigint" | "boolean" | "date" | "datetime" | "time" | "text" | "varchar" | "uuid";
|
||||
export type ORMDefault = "currentTimestamp" | "string" | "boolean" | "number" | "null";
|
||||
export type ColumnConfig = {
|
||||
type: string;
|
||||
length?: string;
|
||||
precision?: number;
|
||||
isNullable: boolean;
|
||||
};
|
||||
export type Primary = {
|
||||
isPrimary: boolean;
|
||||
isGenerated: boolean;
|
||||
generationStrategy: "increment" | "uuid" | "rowid" | "identity";
|
||||
};
|
||||
|
||||
export function getTypeByORM(type: ORMType, nullable: boolean = false, length: number = 255): ColumnConfig {
|
||||
const dbType = process.env.DB_TYPE;
|
||||
|
||||
const typeMap: Record<string, Record<ORMType, string>> = {
|
||||
mysql: {
|
||||
int: "int",
|
||||
bigint: "bigint",
|
||||
boolean: "tinyint",
|
||||
date: "date",
|
||||
datetime: "datetime",
|
||||
time: "time",
|
||||
text: "text",
|
||||
varchar: "varchar",
|
||||
uuid: "varchar",
|
||||
},
|
||||
postgres: {
|
||||
int: "integer",
|
||||
bigint: "bigint",
|
||||
boolean: "boolean",
|
||||
date: "date",
|
||||
datetime: "timestamp",
|
||||
time: "time",
|
||||
text: "text",
|
||||
varchar: "character varying",
|
||||
uuid: "uuid",
|
||||
},
|
||||
sqlite: {
|
||||
int: "integer",
|
||||
bigint: "integer",
|
||||
boolean: "integer",
|
||||
date: "date",
|
||||
datetime: "datetime",
|
||||
time: "text",
|
||||
text: "text",
|
||||
varchar: "varchar",
|
||||
uuid: "varchar",
|
||||
},
|
||||
};
|
||||
|
||||
let obj: ColumnConfig = {
|
||||
type: typeMap[dbType]?.[type] || type,
|
||||
isNullable: nullable,
|
||||
};
|
||||
if (type == "datetime") obj.precision = length;
|
||||
else if (dbType != "sqlite" && (obj.type == "varchar" || type == "varchar")) obj.length = `${length}`;
|
||||
else if (dbType != "postgres" && type == "uuid") obj.length = "36";
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function getDefaultByORM<T = string | null>(type: ORMDefault, data?: string | number | boolean): T {
|
||||
const dbType = process.env.DB_TYPE;
|
||||
|
||||
const typeMap: Record<string, Record<ORMDefault, string | null>> = {
|
||||
mysql: {
|
||||
currentTimestamp: `CURRENT_TIMESTAMP(${data ?? 6})`,
|
||||
string: `'${data ?? ""}'`,
|
||||
boolean: Boolean(data).toString(),
|
||||
number: Number(data).toString(),
|
||||
null: null,
|
||||
},
|
||||
postgres: {
|
||||
currentTimestamp: `now()`,
|
||||
string: `'${data ?? ""}'`,
|
||||
boolean: Boolean(data) == true ? "true" : "false",
|
||||
number: Number(data).toString(),
|
||||
null: null,
|
||||
},
|
||||
sqlite: {
|
||||
currentTimestamp: `datetime('now')`,
|
||||
string: `'${data ?? ""}'`,
|
||||
boolean: Boolean(data) == true ? "1" : "0",
|
||||
number: Number(data).toString(),
|
||||
null: null,
|
||||
},
|
||||
};
|
||||
|
||||
return (typeMap[dbType]?.[type] || type) as T;
|
||||
}
|
||||
|
||||
export const isIncrementPrimary: Primary = {
|
||||
isPrimary: true,
|
||||
isGenerated: true,
|
||||
generationStrategy: "increment",
|
||||
};
|
||||
|
||||
export const isUUIDPrimary: Primary = {
|
||||
isPrimary: true,
|
||||
isGenerated: true,
|
||||
generationStrategy: "uuid",
|
||||
};
|
50
src/routes/admin/configuration/member.ts
Normal file
50
src/routes/admin/configuration/member.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import express, { Request, Response } from "express";
|
||||
import {
|
||||
createMember,
|
||||
deleteMemberById,
|
||||
getAllMembers,
|
||||
getMemberById,
|
||||
getMembersByIds,
|
||||
updateMemberById,
|
||||
} from "../../../controller/admin/configuration/memberController";
|
||||
import PermissionHelper from "../../../helpers/permissionHelper";
|
||||
|
||||
var router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
await getAllMembers(req, res);
|
||||
});
|
||||
|
||||
router.post("/ids", async (req: Request, res: Response) => {
|
||||
await getMembersByIds(req, res);
|
||||
});
|
||||
|
||||
router.get("/:id", async (req: Request, res: Response) => {
|
||||
await getMemberById(req, res);
|
||||
});
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
PermissionHelper.passCheckMiddleware("create", "operation", "force"),
|
||||
async (req: Request, res: Response) => {
|
||||
await createMember(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/:id",
|
||||
PermissionHelper.passCheckMiddleware("update", "operation", "force"),
|
||||
async (req: Request, res: Response) => {
|
||||
await updateMemberById(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:id",
|
||||
PermissionHelper.passCheckMiddleware("delete", "operation", "force"),
|
||||
async (req: Request, res: Response) => {
|
||||
await deleteMemberById(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
27
src/routes/admin/index.ts
Normal file
27
src/routes/admin/index.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import express from "express";
|
||||
import PermissionHelper from "../../helpers/permissionHelper";
|
||||
|
||||
import member from "./configuration/member";
|
||||
|
||||
import role from "./management/role";
|
||||
import user from "./management/user";
|
||||
import invite from "./management/invite";
|
||||
import backup from "./management/backup";
|
||||
|
||||
var router = express.Router({ mergeParams: true });
|
||||
|
||||
router.use("/member", PermissionHelper.passCheckMiddleware("read", "configuration", "force"), member);
|
||||
|
||||
router.use("/role", PermissionHelper.passCheckMiddleware("read", "management", "role"), role);
|
||||
router.use(
|
||||
"/user",
|
||||
PermissionHelper.passCheckSomeMiddleware([
|
||||
{ requiredPermissions: "read", section: "management", module: "user" },
|
||||
{ requiredPermissions: "read", section: "management", module: "role" },
|
||||
]),
|
||||
user
|
||||
);
|
||||
router.use("/invite", PermissionHelper.passCheckMiddleware("read", "management", "user"), invite);
|
||||
router.use("/backup", PermissionHelper.passCheckMiddleware("read", "management", "backup"), backup);
|
||||
|
||||
export default router;
|
88
src/routes/admin/management/backup.ts
Normal file
88
src/routes/admin/management/backup.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
import express, { Request, Response } from "express";
|
||||
import PermissionHelper from "../../../helpers/permissionHelper";
|
||||
import multer from "multer";
|
||||
import {
|
||||
createManualBackup,
|
||||
downloadBackupFile,
|
||||
downloadUploadedBackupFile,
|
||||
getGeneratedBackups,
|
||||
getUploadedBackups,
|
||||
restoreBackupByLocalFile,
|
||||
restoreBackupByUploadedFile,
|
||||
uploadBackupFile,
|
||||
} from "../../../controller/admin/management/backupController";
|
||||
import { FileSystemHelper } from "../../../helpers/fileSystemHelper";
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
FileSystemHelper.createFolder("uploaded-backup");
|
||||
cb(null, "files/uploaded-backup/");
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
cb(null, `${new Date().toISOString().split("T")[0]}_${file.originalname}`);
|
||||
},
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
fileFilter: (req: Request, file, cb) => {
|
||||
if (file.mimetype === "application/json") {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error("Only JSON files are allowed!"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
var router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get("/generated", async (req: Request, res: Response) => {
|
||||
await getGeneratedBackups(req, res);
|
||||
});
|
||||
|
||||
router.get("/generated/:filename", async (req: Request, res: Response) => {
|
||||
await downloadBackupFile(req, res);
|
||||
});
|
||||
|
||||
router.get("/uploaded", async (req: Request, res: Response) => {
|
||||
await getUploadedBackups(req, res);
|
||||
});
|
||||
|
||||
router.get("/uploaded/:filename", async (req: Request, res: Response) => {
|
||||
await downloadUploadedBackupFile(req, res);
|
||||
});
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
PermissionHelper.passCheckMiddleware("create", "management", "backup"),
|
||||
async (req: Request, res: Response) => {
|
||||
await createManualBackup(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/generated/restore",
|
||||
PermissionHelper.passCheckMiddleware("admin", "management", "backup"),
|
||||
async (req: Request, res: Response) => {
|
||||
await restoreBackupByLocalFile(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/uploaded/restore",
|
||||
PermissionHelper.passCheckMiddleware("admin", "management", "backup"),
|
||||
async (req: Request, res: Response) => {
|
||||
await restoreBackupByUploadedFile(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/upload",
|
||||
PermissionHelper.passCheckMiddleware("create", "management", "backup"),
|
||||
upload.single("file"),
|
||||
async (req: Request, res: Response) => {
|
||||
await uploadBackupFile(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
27
src/routes/admin/management/invite.ts
Normal file
27
src/routes/admin/management/invite.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import express, { Request, Response } from "express";
|
||||
import PermissionHelper from "../../../helpers/permissionHelper";
|
||||
import { deleteInvite, getInvites, inviteUser } from "../../../controller/inviteController";
|
||||
|
||||
var router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
await getInvites(req, res);
|
||||
});
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
PermissionHelper.passCheckMiddleware("create", "management", "user"),
|
||||
async (req: Request, res: Response) => {
|
||||
await inviteUser(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:mail",
|
||||
PermissionHelper.passCheckMiddleware("delete", "management", "user"),
|
||||
async (req: Request, res: Response) => {
|
||||
await deleteInvite(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
59
src/routes/admin/management/role.ts
Normal file
59
src/routes/admin/management/role.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import express, { Request, Response } from "express";
|
||||
import PermissionHelper from "../../../helpers/permissionHelper";
|
||||
import {
|
||||
createRole,
|
||||
deleteRole,
|
||||
getAllRoles,
|
||||
getRoleById,
|
||||
getRolePermissions,
|
||||
updateRole,
|
||||
updateRolePermissions,
|
||||
} from "../../../controller/admin/management/roleController";
|
||||
|
||||
var router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
await getAllRoles(req, res);
|
||||
});
|
||||
|
||||
router.get("/:id", async (req: Request, res: Response) => {
|
||||
await getRoleById(req, res);
|
||||
});
|
||||
|
||||
router.get("/:id/permissions", async (req: Request, res: Response) => {
|
||||
await getRolePermissions(req, res);
|
||||
});
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
PermissionHelper.passCheckMiddleware("create", "management", "role"),
|
||||
async (req: Request, res: Response) => {
|
||||
await createRole(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/:id",
|
||||
PermissionHelper.passCheckMiddleware("update", "management", "role"),
|
||||
async (req: Request, res: Response) => {
|
||||
await updateRole(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/:id/permissions",
|
||||
PermissionHelper.passCheckMiddleware("admin", "management", "role"),
|
||||
async (req: Request, res: Response) => {
|
||||
await updateRolePermissions(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:id",
|
||||
PermissionHelper.passCheckMiddleware("delete", "management", "role"),
|
||||
async (req: Request, res: Response) => {
|
||||
await deleteRole(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
65
src/routes/admin/management/user.ts
Normal file
65
src/routes/admin/management/user.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import express, { Request, Response } from "express";
|
||||
import PermissionHelper from "../../../helpers/permissionHelper";
|
||||
import {
|
||||
deleteUser,
|
||||
getAllUsers,
|
||||
getUserById,
|
||||
getUserPermissions,
|
||||
getUserRoles,
|
||||
updateUser,
|
||||
updateUserPermissions,
|
||||
updateUserRoles,
|
||||
} from "../../../controller/admin/management/userController";
|
||||
import { inviteUser } from "../../../controller/inviteController";
|
||||
|
||||
var router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
await getAllUsers(req, res);
|
||||
});
|
||||
|
||||
router.get("/:id", async (req: Request, res: Response) => {
|
||||
await getUserById(req, res);
|
||||
});
|
||||
|
||||
router.get("/:id/permissions", async (req: Request, res: Response) => {
|
||||
await getUserPermissions(req, res);
|
||||
});
|
||||
|
||||
router.get("/:id/roles", async (req: Request, res: Response) => {
|
||||
await getUserRoles(req, res);
|
||||
});
|
||||
|
||||
router.patch(
|
||||
"/:id",
|
||||
PermissionHelper.passCheckMiddleware("update", "management", "user"),
|
||||
async (req: Request, res: Response) => {
|
||||
await updateUser(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/:id/permissions",
|
||||
PermissionHelper.passCheckMiddleware("admin", "management", "user"),
|
||||
async (req: Request, res: Response) => {
|
||||
await updateUserPermissions(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/:id/roles",
|
||||
PermissionHelper.passCheckMiddleware("update", "management", "user"),
|
||||
async (req: Request, res: Response) => {
|
||||
await updateUserRoles(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:id",
|
||||
PermissionHelper.passCheckMiddleware("delete", "management", "user"),
|
||||
async (req: Request, res: Response) => {
|
||||
await deleteUser(req, res);
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
18
src/routes/auth.ts
Normal file
18
src/routes/auth.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import express from "express";
|
||||
import { login, logout, refresh } from "../controller/authController";
|
||||
|
||||
var router = express.Router({ mergeParams: true });
|
||||
|
||||
router.post("/login", async (req, res) => {
|
||||
await login(req, res);
|
||||
});
|
||||
|
||||
router.post("/logout", async (req, res) => {
|
||||
await logout(req, res);
|
||||
});
|
||||
|
||||
router.post("/refresh", async (req, res) => {
|
||||
await refresh(req, res);
|
||||
});
|
||||
|
||||
export default router;
|
88
src/routes/index.ts
Normal file
88
src/routes/index.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
import express from "express";
|
||||
import type { Express, NextFunction, Request, RequestHandler, Response } from "express";
|
||||
import cors from "cors";
|
||||
import helmet from "helmet";
|
||||
import morgan from "morgan";
|
||||
import rateLimit from "express-rate-limit";
|
||||
|
||||
import allowSetup from "../middleware/allowSetup";
|
||||
import authenticate from "../middleware/authenticate";
|
||||
import errorHandler from "../middleware/errorHandler";
|
||||
|
||||
import setup from "./setup";
|
||||
import invite from "./invite";
|
||||
import reset from "./reset";
|
||||
import auth from "./auth";
|
||||
import admin from "./admin/index";
|
||||
import user from "./user";
|
||||
import detectPWA from "../middleware/detectPWA";
|
||||
import server from "./server";
|
||||
import PermissionHelper from "../helpers/permissionHelper";
|
||||
import ms from "ms";
|
||||
import {
|
||||
SECURITY_LIMIT_REQUEST_COUNT,
|
||||
SECURITY_LIMIT_WINDOW,
|
||||
SECURITY_STRICT_LIMIT_REQUEST_COUNT,
|
||||
SECURITY_STRICT_LIMIT_WINDOW,
|
||||
TRUST_PROXY,
|
||||
USE_SECURITY_LIMIT,
|
||||
USE_SECURITY_STRICT_LIMIT,
|
||||
} from "../env.defaults";
|
||||
|
||||
const strictLimiter = rateLimit({
|
||||
windowMs: ms(SECURITY_STRICT_LIMIT_WINDOW),
|
||||
max: SECURITY_STRICT_LIMIT_REQUEST_COUNT,
|
||||
message: `Zu viele Anmeldeversuche innerhalb von ${SECURITY_STRICT_LIMIT_WINDOW}. Bitte warten.`,
|
||||
skipSuccessfulRequests: true,
|
||||
skip: () => {
|
||||
return USE_SECURITY_STRICT_LIMIT == "false";
|
||||
},
|
||||
});
|
||||
|
||||
const generalLimiter = rateLimit({
|
||||
windowMs: ms(SECURITY_LIMIT_WINDOW),
|
||||
max: SECURITY_LIMIT_REQUEST_COUNT,
|
||||
message: `Zu viele Anfragen innerhalb von ${SECURITY_LIMIT_WINDOW}. Bitte warten.`,
|
||||
skipSuccessfulRequests: true,
|
||||
skip: () => {
|
||||
return USE_SECURITY_LIMIT == "false";
|
||||
},
|
||||
});
|
||||
|
||||
function excludePaths(middleware: RequestHandler, excludedPaths: Array<string>) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
if (excludedPaths.includes(req.path)) {
|
||||
return next();
|
||||
}
|
||||
return middleware(req, res, next);
|
||||
};
|
||||
}
|
||||
|
||||
export default (app: Express) => {
|
||||
if (TRUST_PROXY) {
|
||||
app.set("trust proxy", TRUST_PROXY);
|
||||
}
|
||||
app.set("query parser", "extended");
|
||||
app.use(cors());
|
||||
app.options("*", cors());
|
||||
app.use(helmet());
|
||||
app.use(morgan("short"));
|
||||
app.use(express.json());
|
||||
app.use(
|
||||
express.urlencoded({
|
||||
extended: true,
|
||||
})
|
||||
);
|
||||
|
||||
app.use(detectPWA);
|
||||
app.use("/api/setup", strictLimiter, allowSetup, setup);
|
||||
app.use("/api/reset", strictLimiter, reset);
|
||||
app.use("/api/invite", strictLimiter, invite);
|
||||
app.use("/api/auth", strictLimiter, auth);
|
||||
app.use(authenticate);
|
||||
app.use(excludePaths(generalLimiter, ["/synchronize"]));
|
||||
app.use("/api/admin", admin);
|
||||
app.use("/api/user", user);
|
||||
app.use("/api/server", PermissionHelper.isAdminMiddleware(), server);
|
||||
app.use(errorHandler);
|
||||
};
|
16
src/routes/invite.ts
Normal file
16
src/routes/invite.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import express from "express";
|
||||
import { isSetup } from "../controller/setupController";
|
||||
import { finishInvite, inviteUser, verifyInvite } from "../controller/inviteController";
|
||||
import ParamaterPassCheckHelper from "../helpers/parameterPassCheckHelper";
|
||||
|
||||
var router = express.Router({ mergeParams: true });
|
||||
|
||||
router.post("/verify", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token"]), async (req, res) => {
|
||||
await verifyInvite(req, res);
|
||||
});
|
||||
|
||||
router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => {
|
||||
await finishInvite(req, res);
|
||||
});
|
||||
|
||||
export default router;
|
19
src/routes/reset.ts
Normal file
19
src/routes/reset.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import express from "express";
|
||||
import ParamaterPassCheckHelper from "../helpers/parameterPassCheckHelper";
|
||||
import { finishReset, startReset, verifyReset } from "../controller/resetController";
|
||||
|
||||
var router = express.Router({ mergeParams: true });
|
||||
|
||||
router.post("/verify", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token"]), async (req, res) => {
|
||||
await verifyReset(req, res);
|
||||
});
|
||||
|
||||
router.post("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["username"]), async (req, res) => {
|
||||
await startReset(req, res);
|
||||
});
|
||||
|
||||
router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => {
|
||||
await finishReset(req, res);
|
||||
});
|
||||
|
||||
export default router;
|
35
src/routes/server.ts
Normal file
35
src/routes/server.ts
Normal file
|
@ -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-operation-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-operation/releases.rss");
|
||||
res.json(feed);
|
||||
});
|
||||
|
||||
export default router;
|
28
src/routes/setup.ts
Normal file
28
src/routes/setup.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import express from "express";
|
||||
import { isSetup } from "../controller/setupController";
|
||||
import { finishInvite, inviteUser, verifyInvite } from "../controller/inviteController";
|
||||
import ParamaterPassCheckHelper from "../helpers/parameterPassCheckHelper";
|
||||
|
||||
var router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
await isSetup(req, res);
|
||||
});
|
||||
|
||||
router.post("/verify", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token"]), async (req, res) => {
|
||||
await verifyInvite(req, res);
|
||||
});
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
ParamaterPassCheckHelper.requiredIncludedMiddleware(["username", "mail", "firstname", "lastname"]),
|
||||
async (req, res) => {
|
||||
await inviteUser(req, res, false);
|
||||
}
|
||||
);
|
||||
|
||||
router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => {
|
||||
await finishInvite(req, res, true);
|
||||
});
|
||||
|
||||
export default router;
|
26
src/routes/user.ts
Normal file
26
src/routes/user.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import express from "express";
|
||||
import { getMeById, getMyTotp, transferOwnership, updateMe, verifyMyTotp } from "../controller/userController";
|
||||
|
||||
var router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get("/me", async (req, res) => {
|
||||
await getMeById(req, res);
|
||||
});
|
||||
|
||||
router.get("/totp", async (req, res) => {
|
||||
await getMyTotp(req, res);
|
||||
});
|
||||
|
||||
router.post("/verify", async (req, res) => {
|
||||
await verifyMyTotp(req, res);
|
||||
});
|
||||
|
||||
router.put("/transferOwner", async (req, res) => {
|
||||
await transferOwnership(req, res);
|
||||
});
|
||||
|
||||
router.patch("/me", async (req, res) => {
|
||||
await updateMe(req, res);
|
||||
});
|
||||
|
||||
export default router;
|
80
src/service/configuration/memberService.ts
Normal file
80
src/service/configuration/memberService.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import { dataSource } from "../../data-source";
|
||||
import { member } from "../../entity/configuration/member";
|
||||
import DatabaseActionException from "../../exceptions/databaseActionException";
|
||||
|
||||
export default abstract class MemberService {
|
||||
/**
|
||||
* @description get all members
|
||||
* @returns {Promise<[Array<member>, number]>}
|
||||
*/
|
||||
static async getAll({
|
||||
offset = 0,
|
||||
count = 25,
|
||||
search = "",
|
||||
noLimit = false,
|
||||
ids = [],
|
||||
}: {
|
||||
offset?: number;
|
||||
count?: number;
|
||||
search?: string;
|
||||
noLimit?: boolean;
|
||||
ids?: Array<string>;
|
||||
}): Promise<[Array<member>, number]> {
|
||||
let query = dataSource.getRepository(member).createQueryBuilder("member");
|
||||
|
||||
if (search != "") {
|
||||
search.split(" ").forEach((term, index) => {
|
||||
const searchQuery = `%${term}%`;
|
||||
const dynamic = "searchQuery" + Math.random().toString(36).substring(2);
|
||||
if (index == 0) {
|
||||
query = query.where(`member.firstname LIKE :${dynamic} OR member.lastname LIKE :${dynamic}`, {
|
||||
[dynamic]: searchQuery,
|
||||
});
|
||||
} else {
|
||||
query = query.orWhere(`member.firstname LIKE :${dynamic} OR member.lastname LIKE :${dynamic}`, {
|
||||
[dynamic]: searchQuery,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (ids.length != 0) {
|
||||
query = query.where("member.id IN (:...ids)", { ids: ids });
|
||||
}
|
||||
|
||||
if (!noLimit) {
|
||||
query = query.offset(offset).limit(count);
|
||||
}
|
||||
|
||||
return await query
|
||||
.orderBy("member.lastname")
|
||||
.addOrderBy("member.firstname")
|
||||
.addOrderBy("member.nameaffix")
|
||||
.getManyAndCount()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("SELECT", "member", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get member by id
|
||||
* @param {string} id
|
||||
* @returns {Promise<member>}
|
||||
*/
|
||||
static async getById(id: string): Promise<member> {
|
||||
return dataSource
|
||||
.getRepository(member)
|
||||
.createQueryBuilder("member")
|
||||
.where("member.id = :id", { id: id })
|
||||
.getOneOrFail()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("SELECT", "member", err);
|
||||
});
|
||||
}
|
||||
}
|
44
src/service/management/inviteService.ts
Normal file
44
src/service/management/inviteService.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { dataSource } from "../../data-source";
|
||||
import { invite } from "../../entity/management/invite";
|
||||
import DatabaseActionException from "../../exceptions/databaseActionException";
|
||||
import InternalException from "../../exceptions/internalException";
|
||||
|
||||
export default abstract class InviteService {
|
||||
/**
|
||||
* @description get all invites
|
||||
* @returns {Promise<Array<invite>>}
|
||||
*/
|
||||
static async getAll(): Promise<Array<invite>> {
|
||||
return await dataSource
|
||||
.getRepository(invite)
|
||||
.createQueryBuilder("invite")
|
||||
.getMany()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("SELECT", "invite", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get invite by id
|
||||
* @param mail string
|
||||
* @param token string
|
||||
* @returns {Promise<invite>}
|
||||
*/
|
||||
static async getByMailAndToken(mail: string, token: string): Promise<invite> {
|
||||
return await dataSource
|
||||
.getRepository(invite)
|
||||
.createQueryBuilder("invite")
|
||||
.where("invite.mail = :mail", { mail: mail })
|
||||
.andWhere("invite.token = :token", { token: token })
|
||||
.getOneOrFail()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("SELECT", "invite", err);
|
||||
});
|
||||
}
|
||||
}
|
45
src/service/management/rolePermissionService.ts
Normal file
45
src/service/management/rolePermissionService.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { dataSource } from "../../data-source";
|
||||
import { rolePermission } from "../../entity/management/role_permission";
|
||||
import { userPermission } from "../../entity/management/user_permission";
|
||||
import DatabaseActionException from "../../exceptions/databaseActionException";
|
||||
import InternalException from "../../exceptions/internalException";
|
||||
|
||||
export default abstract class RolePermissionService {
|
||||
/**
|
||||
* @description get permission by role
|
||||
* @param roleId number
|
||||
* @returns {Promise<Array<rolePermission>>}
|
||||
*/
|
||||
static async getByRole(roleId: number): Promise<Array<rolePermission>> {
|
||||
return await dataSource
|
||||
.getRepository(rolePermission)
|
||||
.createQueryBuilder("permission")
|
||||
.where("permission.roleId = :roleId", { roleId: roleId })
|
||||
.getMany()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("SELECT", "rolePermission", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get permission by roles
|
||||
* @param roleIds Array<number>
|
||||
* @returns {Promise<Array<rolePermission>>}
|
||||
*/
|
||||
static async getByRoles(roleIds: Array<number>): Promise<Array<rolePermission>> {
|
||||
return await dataSource
|
||||
.getRepository(rolePermission)
|
||||
.createQueryBuilder("permission")
|
||||
.where("permission.roleId IN (:...roleIds)", { roleIds: roleIds })
|
||||
.getMany()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("SELECT", "rolePermission", err);
|
||||
});
|
||||
}
|
||||
}
|
45
src/service/management/roleService.ts
Normal file
45
src/service/management/roleService.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { dataSource } from "../../data-source";
|
||||
import { role } from "../../entity/management/role";
|
||||
import DatabaseActionException from "../../exceptions/databaseActionException";
|
||||
import InternalException from "../../exceptions/internalException";
|
||||
|
||||
export default abstract class RoleService {
|
||||
/**
|
||||
* @description get roles
|
||||
* @returns {Promise<Array<role>>}
|
||||
*/
|
||||
static async getAll(): Promise<Array<role>> {
|
||||
return await dataSource
|
||||
.getRepository(role)
|
||||
.createQueryBuilder("role")
|
||||
.leftJoinAndSelect("role.permissions", "role_permissions")
|
||||
.orderBy("role", "ASC")
|
||||
.getMany()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("SELECT", "roles", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get role by id
|
||||
* @param id number
|
||||
* @returns {Promise<role>}
|
||||
*/
|
||||
static async getById(id: number): Promise<role> {
|
||||
return await dataSource
|
||||
.getRepository(role)
|
||||
.createQueryBuilder("role")
|
||||
.leftJoinAndSelect("role.permissions", "role_permissions")
|
||||
.where("role.id = :id", { id: id })
|
||||
.getOneOrFail()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("SELECT", "role", err);
|
||||
});
|
||||
}
|
||||
}
|
25
src/service/management/userPermissionService.ts
Normal file
25
src/service/management/userPermissionService.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { dataSource } from "../../data-source";
|
||||
import { userPermission } from "../../entity/management/user_permission";
|
||||
import DatabaseActionException from "../../exceptions/databaseActionException";
|
||||
import InternalException from "../../exceptions/internalException";
|
||||
|
||||
export default abstract class UserPermissionService {
|
||||
/**
|
||||
* @description get permission by user
|
||||
* @param userId string
|
||||
* @returns {Promise<Array<userPermission>>}
|
||||
*/
|
||||
static async getByUser(userId: string): Promise<Array<userPermission>> {
|
||||
return await dataSource
|
||||
.getRepository(userPermission)
|
||||
.createQueryBuilder("permission")
|
||||
.where({ userId: userId })
|
||||
.getMany()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("SELECT", "userPermission", err);
|
||||
});
|
||||
}
|
||||
}
|
132
src/service/management/userService.ts
Normal file
132
src/service/management/userService.ts
Normal file
|
@ -0,0 +1,132 @@
|
|||
import { dataSource } from "../../data-source";
|
||||
import { role } from "../../entity/management/role";
|
||||
import { user } from "../../entity/management/user";
|
||||
import DatabaseActionException from "../../exceptions/databaseActionException";
|
||||
import InternalException from "../../exceptions/internalException";
|
||||
|
||||
export default abstract class UserService {
|
||||
/**
|
||||
* @description get users
|
||||
* @returns {Promise<Array<user>>}
|
||||
*/
|
||||
static async getAll(): Promise<Array<user>> {
|
||||
return await dataSource
|
||||
.getRepository(user)
|
||||
.createQueryBuilder("user")
|
||||
.leftJoinAndSelect("user.roles", "roles")
|
||||
.leftJoinAndSelect("user.permissions", "permissions")
|
||||
.leftJoinAndSelect("roles.permissions", "role_permissions")
|
||||
.orderBy("firstname", "ASC")
|
||||
.addOrderBy("lastname", "ASC")
|
||||
.getMany()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("SELECT", "user", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get user by id
|
||||
* @param id string
|
||||
* @returns {Promise<user>}
|
||||
*/
|
||||
static async getById(id: string): Promise<user> {
|
||||
return await dataSource
|
||||
.getRepository(user)
|
||||
.createQueryBuilder("user")
|
||||
.leftJoinAndSelect("user.roles", "roles")
|
||||
.leftJoinAndSelect("user.permissions", "permissions")
|
||||
.leftJoinAndSelect("roles.permissions", "role_permissions")
|
||||
.where("user.id = :id", { id: id })
|
||||
.getOneOrFail()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("SELECT", "user", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get user by username
|
||||
* @param username string
|
||||
* @returns {Promise<user>}
|
||||
*/
|
||||
static async getByUsername(username: string): Promise<user> {
|
||||
return await dataSource
|
||||
.getRepository(user)
|
||||
.createQueryBuilder("user")
|
||||
.select()
|
||||
.where("user.username = :username", { username: username })
|
||||
.getOneOrFail()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("SELECT", "user", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get users by mail or username
|
||||
* @param username string
|
||||
* @param mail string
|
||||
* @returns {Promise<Array<user>>}
|
||||
*/
|
||||
static async getByMailOrUsername(mail?: string, username?: string): Promise<Array<user>> {
|
||||
return await dataSource
|
||||
.getRepository(user)
|
||||
.createQueryBuilder("user")
|
||||
.select()
|
||||
.where("user.mail = :mail", { mail: mail })
|
||||
.orWhere("user.username = :username", { username: username })
|
||||
.getMany()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("SELECT", "user", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get count of users
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
static async count(): Promise<number> {
|
||||
return await dataSource
|
||||
.getRepository(user)
|
||||
.createQueryBuilder("user")
|
||||
.select()
|
||||
.getCount()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("COUNT", "users", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get roles assigned to user
|
||||
* @param userId string
|
||||
* @returns {Promise<Array<role>>}
|
||||
*/
|
||||
static async getAssignedRolesByUserId(userId: string): Promise<Array<role>> {
|
||||
return await dataSource
|
||||
.getRepository(user)
|
||||
.createQueryBuilder("user")
|
||||
.leftJoinAndSelect("user.roles", "roles")
|
||||
.leftJoinAndSelect("roles.permissions", "role_permissions")
|
||||
.where("user.id = :id", { id: userId })
|
||||
.getOneOrFail()
|
||||
.then((res) => {
|
||||
return res.roles;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("SELECT", "userRoles", err);
|
||||
});
|
||||
}
|
||||
}
|
26
src/service/refreshService.ts
Normal file
26
src/service/refreshService.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { dataSource } from "../data-source";
|
||||
import { refresh } from "../entity/refresh";
|
||||
import InternalException from "../exceptions/internalException";
|
||||
|
||||
export default abstract class RefreshService {
|
||||
/**
|
||||
* @description get refresh by token
|
||||
* @param token string
|
||||
* @returns {Promise<refresh>}
|
||||
*/
|
||||
static async getByToken(token: string): Promise<refresh> {
|
||||
return await dataSource
|
||||
.getRepository(refresh)
|
||||
.createQueryBuilder("refresh")
|
||||
.leftJoinAndSelect("refresh.user", "user")
|
||||
.where("refresh.token = :token", { token: token })
|
||||
.andWhere("refresh.expiry >= :expiry", { expiry: new Date() })
|
||||
.getOneOrFail()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new InternalException("refresh not found", err);
|
||||
});
|
||||
}
|
||||
}
|
27
src/service/resetService.ts
Normal file
27
src/service/resetService.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { dataSource } from "../data-source";
|
||||
import { reset } from "../entity/reset";
|
||||
import DatabaseActionException from "../exceptions/databaseActionException";
|
||||
import InternalException from "../exceptions/internalException";
|
||||
|
||||
export default abstract class ResetService {
|
||||
/**
|
||||
* @description get reset by id
|
||||
* @param mail string
|
||||
* @param token string
|
||||
* @returns {Promise<reset>}
|
||||
*/
|
||||
static async getByMailAndToken(mail: string, token: string): Promise<reset> {
|
||||
return await dataSource
|
||||
.getRepository(reset)
|
||||
.createQueryBuilder("reset")
|
||||
.where("reset.mail = :mail", { mail: mail })
|
||||
.andWhere("reset.token = :token", { token: token })
|
||||
.getOneOrFail()
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new DatabaseActionException("SELECT", "reset", err);
|
||||
});
|
||||
}
|
||||
}
|
19
src/type/jwtTypes.ts
Normal file
19
src/type/jwtTypes.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { PermissionObject } from "./permissionTypes";
|
||||
|
||||
export type JWTData = {
|
||||
[key: string]: string | number | boolean | PermissionObject;
|
||||
};
|
||||
|
||||
export type JWTToken = {
|
||||
userId: string;
|
||||
mail: string;
|
||||
username: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
isOwner: boolean;
|
||||
permissions: PermissionObject;
|
||||
} & JWTData;
|
||||
|
||||
export type JWTRefresh = {
|
||||
userId: number;
|
||||
} & JWTData;
|
33
src/type/permissionTypes.ts
Normal file
33
src/type/permissionTypes.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
export type PermissionSection = "operation" | "configuration" | "management";
|
||||
|
||||
export type PermissionModule = "mission" | "force" | "user" | "role" | "backup";
|
||||
|
||||
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 type SectionsAndModulesObject = {
|
||||
[section in PermissionSection]: Array<PermissionModule>;
|
||||
};
|
||||
|
||||
export const permissionSections: Array<PermissionSection> = ["operation", "configuration", "management"];
|
||||
export const permissionModules: Array<PermissionModule> = ["mission", "force", "user", "role", "backup"];
|
||||
export const permissionTypes: Array<PermissionType> = ["read", "create", "update", "delete"];
|
||||
export const sectionsAndModules: SectionsAndModulesObject = {
|
||||
operation: ["mission"],
|
||||
configuration: ["force"],
|
||||
management: ["user", "role", "backup"],
|
||||
};
|
6
src/viewmodel/admin/configuration/member.models.ts
Normal file
6
src/viewmodel/admin/configuration/member.models.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export interface MemberViewModel {
|
||||
id: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
nameaffix: string;
|
||||
}
|
6
src/viewmodel/admin/management/invite.models.ts
Normal file
6
src/viewmodel/admin/management/invite.models.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export interface InviteViewModel {
|
||||
mail: string;
|
||||
username: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
}
|
7
src/viewmodel/admin/management/role.models.ts
Normal file
7
src/viewmodel/admin/management/role.models.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { PermissionObject } from "../../../type/permissionTypes";
|
||||
|
||||
export interface RoleViewModel {
|
||||
id: number;
|
||||
permissions: PermissionObject;
|
||||
role: string;
|
||||
}
|
14
src/viewmodel/admin/management/user.models.ts
Normal file
14
src/viewmodel/admin/management/user.models.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { PermissionObject } from "../../../type/permissionTypes";
|
||||
import { RoleViewModel } from "./role.models";
|
||||
|
||||
export interface UserViewModel {
|
||||
id: string;
|
||||
username: string;
|
||||
mail: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
isOwner: boolean;
|
||||
permissions: PermissionObject;
|
||||
roles: Array<RoleViewModel>;
|
||||
permissions_total: PermissionObject;
|
||||
}
|
1
src/viewmodel/permissionViewModel.ts
Normal file
1
src/viewmodel/permissionViewModel.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export interface PermissionViewModel {}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue