From c7d53c88bc9fd2630b2538762146bbc1c6c777ba Mon Sep 17 00:00:00 2001 From: tw-blue Date: Wed, 10 Dec 2025 22:11:30 +0100 Subject: [PATCH] finalize migration (yay) --- .prettierrc | 14 ++ package.json | 6 +- packages/api/.vscode/launch.json | 17 +- packages/api/package.json | 13 +- .../20251209225722_marker/migration.sql | 21 ++ packages/api/prisma/schema.prisma | 2 +- packages/api/scripts/dev.sh | 5 - .../api/src/controllers/membersController.ts | 13 +- packages/api/src/dtos/member.ts | 61 ++++- packages/api/src/index.ts | 142 +++++++---- packages/api/src/middleware/authentication.ts | 45 ++-- packages/api/src/services/db/prisma.ts | 6 +- .../services/keycloak/admin/keycloak-admin.ts | 26 +- .../keycloak/admin/nodejs-api.dev.json | 10 + .../services/keycloak/admin/nodejs-api.json | 60 ----- .../services/keycloak/user/keycloak-user.ts | 53 ++++- .../user/{keycloak.json => keycloak.dev.json} | 0 packages/api/src/services/membersService.ts | 21 +- packages/api/tsconfig.json | 3 +- packages/frontend/package.json | 7 +- packages/frontend/src/app/app.config.ts | 11 +- .../delete-member-dialog.component.ts | 30 ++- .../edit-member-dialog.component.ts | 4 +- .../new-member/new-member.component.html | 76 +++--- .../new-member/new-member.component.ts | 2 +- .../components/view-members/view-members.html | 205 ++++++++-------- .../components/view-members/view-members.scss | 21 ++ .../components/view-members/view-members.ts | 224 +++++++++++++----- .../frontend/src/app/pipes/date-pipe.pipe.ts | 30 ++- .../src/app/services/authentication.ts | 1 + packages/frontend/src/app/util/api.ts | 10 + packages/frontend/src/app/util/member.ts | 55 +++-- pnpm-lock.yaml | 165 ++++++++----- pnpm-workspace.yaml | 2 + scripts/angular.ps1 | 2 + scripts/api-ts.ps1 | 6 + scripts/apigen.ps1 | 48 ++++ scripts/dev.ps1 | 44 ++++ scripts/docker.ps1 | 2 + tsoa.json | 2 +- 40 files changed, 966 insertions(+), 499 deletions(-) create mode 100644 .prettierrc create mode 100644 packages/api/prisma/migrations/20251209225722_marker/migration.sql delete mode 100644 packages/api/scripts/dev.sh create mode 100644 packages/api/src/services/keycloak/admin/nodejs-api.dev.json delete mode 100644 packages/api/src/services/keycloak/admin/nodejs-api.json rename packages/api/src/services/keycloak/user/{keycloak.json => keycloak.dev.json} (100%) create mode 100644 packages/frontend/src/app/util/api.ts create mode 100644 scripts/angular.ps1 create mode 100644 scripts/api-ts.ps1 create mode 100644 scripts/apigen.ps1 create mode 100644 scripts/dev.ps1 create mode 100644 scripts/docker.ps1 diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..01779e7 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,14 @@ +{ + "tabWidth": 4, + "arrowParens": "avoid", + "bracketSameLine": true, + "semi": true, + "overrides": [ + { + "files": "*.yml", + "options": { + "tabWidth": 2 + } + } + ] +} diff --git a/package.json b/package.json index 744a712..a5e1a36 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,15 @@ "description": "", "private": true, "scripts": { - "generate": "tsoa spec-and-routes" + "generate": "tsoa spec-and-routes", + "dev": "pwsh ./scripts/dev.ps1" }, "author": "", "license": "UNLICENSED", - "packageManager": "pnpm@10.20.0", + "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a", "type": "module", "devDependencies": { + "nodemon": "^3.1.11", "tsoa": "^6.6.0" } } diff --git a/packages/api/.vscode/launch.json b/packages/api/.vscode/launch.json index 337b7eb..6f3aed5 100644 --- a/packages/api/.vscode/launch.json +++ b/packages/api/.vscode/launch.json @@ -8,13 +8,9 @@ "type": "node", "request": "launch", "name": "Launch Program", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "program": "${workspaceFolder}\\dist\\index.js", - "outFiles": [ - "${workspaceFolder}/**/*.js" - ], + "outFiles": ["${workspaceFolder}/**/*.js"], "env": { "TKD_DB_USER": "webapi", "TKD_DB_PASSWORD": "devpassword", @@ -22,6 +18,15 @@ "TKD_COOKIE_SECRET": "devsecret" }, "runtimeExecutable": "C:\\Users\\Toni\\AppData\\Roaming\\fnm\\aliases\\default\\node" + }, + { + "name": "Attach to Gandalf", + "type": "node", + "request": "attach", + "address": "192.168.2.250", + "port": 9229, + "localRoot": "${workspaceFolder}", + "remoteRoot": "/var/www/api" } ] } diff --git a/packages/api/package.json b/packages/api/package.json index 6db66e5..667d12e 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -3,14 +3,15 @@ "version": "1.0.0", "scripts": { "start": "node dist/index.js", - "build": "tsc -p ." + "build": "tsc -p .", + "dev": "pnpm -w run dev" }, "type": "module", "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a", "private": true, "devDependencies": { - "@types/cors": "^2.8.19", "@types/connect-pg-simple": "^7.0.3", + "@types/cors": "^2.8.19", "@types/express": "^5.0.5", "@types/express-session": "^1.18.2", "@types/node": "^24.9.2", @@ -20,17 +21,19 @@ "typescript": "~5.9.3" }, "dependencies": { - "cors": "^2.8.5", - "rxjs": "~7.8.2", "@keycloak/keycloak-admin-client": "^26.4.4", "@prisma/adapter-pg": "^7.0.1", "@prisma/client": "^7.0.1", "@tsoa/runtime": "^6.6.0", "connect-pg-simple": "^10.0.0", + "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", "express-session": "^1.18.2", + "helmet": "^8.1.0", "keycloak-connect": "^26.1.1", - "pg": "^8.16.3" + "pg": "^8.16.3", + "rxjs": "~7.8.2", + "tsoa": "^6.6.0" } } diff --git a/packages/api/prisma/migrations/20251209225722_marker/migration.sql b/packages/api/prisma/migrations/20251209225722_marker/migration.sql new file mode 100644 index 0000000..173b371 --- /dev/null +++ b/packages/api/prisma/migrations/20251209225722_marker/migration.sql @@ -0,0 +1,21 @@ +/* + Warnings: + + - You are about to drop the column `uvertrag` on the `Member` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Member" DROP COLUMN "uvertrag", +ADD COLUMN "marker" BOOLEAN; + +-- CreateTable +CREATE TABLE "session" ( + "sid" VARCHAR NOT NULL, + "sess" JSON NOT NULL, + "expire" TIMESTAMP(6) NOT NULL, + + CONSTRAINT "session_pkey" PRIMARY KEY ("sid") +); + +-- CreateIndex +CREATE INDEX "IDX_session_expire" ON "session"("expire"); diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index 5b09761..eff0f24 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -15,12 +15,12 @@ model Member { geburtsort String? @db.VarChar geschlecht String? @db.VarChar passnummer String? @db.VarChar - uvertrag Boolean? verein Boolean? graduierung String? @db.VarChar letztePruefung DateTime? @db.Date kontakt String? @db.VarChar kuendigungzum DateTime? @db.Date + marker Boolean? } model session { diff --git a/packages/api/scripts/dev.sh b/packages/api/scripts/dev.sh deleted file mode 100644 index 1886c44..0000000 --- a/packages/api/scripts/dev.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -echo "Starting development environment..." - -pnpm -v diff --git a/packages/api/src/controllers/membersController.ts b/packages/api/src/controllers/membersController.ts index 9a5c475..9dd55dc 100644 --- a/packages/api/src/controllers/membersController.ts +++ b/packages/api/src/controllers/membersController.ts @@ -6,6 +6,7 @@ import { Path, Post, Put, + Query, Request, Response, Route, @@ -14,7 +15,7 @@ import { } from "@tsoa/runtime"; import express from "express"; import { Claim } from "../dtos/claim.js"; -import type { Member, MemberCreateArgs } from "../dtos/member.js"; +import type { MemberCreateArgs, MemberDto } from "../dtos/member.js"; import { inject } from "../infrastructure/di/index.js"; import { ErrorStates } from "../infrastructure/errorStates.js"; import { KC_SECURITY_NAME } from "../services/keycloak/user/index.js"; @@ -28,7 +29,7 @@ export class MembersContoller extends Controller { @Get() public async getMembers( @Request() request: express.Request, - ): Promise { + ): Promise { return await this.membersService.getMembers(request); } @@ -39,7 +40,7 @@ export class MembersContoller extends Controller { public async getMember( @Path() memberId: string, @Request() request: express.Request, - ): Promise { + ): Promise { try { const foundMember = await this.membersService.getMemberById( memberId, @@ -64,7 +65,7 @@ export class MembersContoller extends Controller { public async createMember( @Body() createArgs: MemberCreateArgs, @Request() request: express.Request, - ): Promise { + ): Promise { return await this.membersService.createMember(createArgs, request); } @@ -74,7 +75,7 @@ export class MembersContoller extends Controller { @Response(404, "Member not found") @Put() public async updateMember( - @Body() member: Member, + @Body() member: MemberDto, @Request() request: express.Request, ): Promise { try { @@ -97,7 +98,7 @@ export class MembersContoller extends Controller { @Delete("{memberId}") public async scheduleDeletion( @Path() memberId: string, - @Body() cancelDate: Date, + @Query() cancelDate: Date, @Request() request: express.Request, ) { await this.membersService.scheduleDeletion( diff --git a/packages/api/src/dtos/member.ts b/packages/api/src/dtos/member.ts index d47cf34..c98f83a 100644 --- a/packages/api/src/dtos/member.ts +++ b/packages/api/src/dtos/member.ts @@ -1,27 +1,39 @@ import { type MemberModel as DbMember } from "../generated/prisma/models.js"; -export interface Member { +export interface MemberDto { id: string | null; vorname: string | null; nachname: string | null; - geburtsdatum: Date | null; + geburtsdatum: string | null; geburtsort: string | null; geschlecht: string | null; passnummer: string | null; - uvertrag: boolean | null; verein: boolean | null; graduierung: string | null; - letztePruefung: Date | null; + letztePruefung: string | null; kontakt: string | null; - kuendigungzum: Date | null; - loeschenam: Date | null; + kuendigungzum: string | null; + marker: boolean | null; } -export type MemberCreateArgs = Omit; +export class MemberCreateArgs implements Omit { + vorname!: string | null; + nachname!: string | null; + geburtsdatum!: string | null; + geburtsort!: string | null; + geschlecht!: string | null; + passnummer!: string | null; + verein!: boolean | null; + graduierung!: string | null; + letztePruefung!: string | null; + kontakt!: string | null; + kuendigungzum!: string | null; + marker!: boolean | null; +} -export function mapDbMemberToMember(dbMember: DbMember): Member { +export function mapDbMemberToMember(dbMember: DbMember): MemberDto { return { - geburtsdatum: dbMember.geburtsdatum, + geburtsdatum: dbMember.geburtsdatum?.toDateString() ?? null, id: dbMember.id, nachname: dbMember.nachname, geburtsort: dbMember.geburtsort, @@ -29,11 +41,34 @@ export function mapDbMemberToMember(dbMember: DbMember): Member { graduierung: dbMember.graduierung, vorname: dbMember.vorname, kontakt: dbMember.kontakt, - kuendigungzum: dbMember.kuendigungzum, - letztePruefung: dbMember.letztePruefung, + kuendigungzum: dbMember.kuendigungzum?.toDateString() ?? null, + letztePruefung: dbMember.letztePruefung?.toDateString() ?? null, passnummer: dbMember.passnummer, - uvertrag: dbMember.uvertrag, verein: dbMember.verein, - loeschenam: new Date(), //TODO + marker: dbMember.marker, + }; +} + +export function mapMemberDtoToDbMember(member: MemberDto): DbMember { + return { + id: member.id!, + vorname: member.vorname, + nachname: member.nachname, + geburtsdatum: member.geburtsdatum + ? new Date(member.geburtsdatum) + : null, + geburtsort: member.geburtsort, + geschlecht: member.geschlecht, + passnummer: member.passnummer, + verein: member.verein, + graduierung: member.graduierung, + letztePruefung: member.letztePruefung + ? new Date(member.letztePruefung) + : null, + kontakt: member.kontakt, + kuendigungzum: member.kuendigungzum + ? new Date(member.kuendigungzum) + : null, + marker: member.marker, }; } diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index a9f8bd4..f07f9a6 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,67 +1,113 @@ +import constants from "constants"; import cors from "cors"; import express, { type NextFunction, type Request, type Response, } from "express"; +import fs from "fs"; +import https from "https"; import { RegisterRoutes } from "./generated/routes.js"; import { inject } from "./infrastructure/di/injector.js"; import { sessionHandler } from "./infrastructure/sessionHandler.js"; import { EnvironmentService } from "./services/environmentService.js"; -import { keycloak } from "./services/keycloak/user/index.js"; +import { KeycloakUser } from "./services/keycloak/user/keycloak-user.js"; +// import helmet from "helmet"; +// TODO import http2 from 'http2'; -const app = express(); +async function run() { + const app = express(); + const keycloakUser = inject(KeycloakUser); -app.use(sessionHandler); -app.use(keycloak.middleware()); + //TODO app.use(helmet.hsts()); this and other helmets + app.use( + cors({ + origin: [ + "https://taekwondo-chemnitz.toni714.de", + "https://tcc-1-ev-intern.web.app", + "http://localhost:4200", + "https://localhost:4200", + "http://localhost:8080", + ], + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + //TODO reevaluate + }), + ); + app.use(sessionHandler); + app.use(await keycloakUser.middleware()); -app.use(express.urlencoded({ extended: true })); -app.use(express.json()); -app.use( - cors({ - origin: [ - "https://taekwondo-chemnitz.toni714.de", - "https://tcc-1-ev-intern.web.app", - "http://localhost:4200", - "https://localhost:4200", - "http://localhost:3000", - "http://localhost:8080", - ], - //TODO reevaluate - }), -); + app.use(express.urlencoded({ extended: true })); + app.use(express.json()); -// const dbService = inject(DatabaseService); -// app.use(async (req, res, next) => { -// await dbService.createTransaction(async prisma => { -// (req as any).transaction = prisma; -// return new Promise((resolve, reject) => { -// res.on("finish", resolve); -// res.on("close", resolve); -// res.on("error", reject); -// next(); -// }); -// }); -// }); + // const dbService = inject(DatabaseService); + // app.use(async (req, res, next) => { + // await dbService.createTransaction(async prisma => { + // (req as any).transaction = prisma; + // return new Promise((resolve, reject) => { + // res.on("finish", resolve); + // res.on("close", resolve); + // res.on("error", reject); + // next(); + // }); + // }); + // }); -RegisterRoutes(app); + RegisterRoutes(app); -const environment = inject(EnvironmentService); -// wants to be last -app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => { - res.status(500); - res.send(JSON.stringify(err)); - throw err; -}); + const environment = inject(EnvironmentService); + // wants to be last + app.use( + (err: unknown, _req: Request, res: Response, _next: NextFunction) => { + console.dir(err); + const ex = JSON.stringify(err); + res.header("Content-Length", `${ex.length}`); + res.status(500).send(ex); + throw err; + }, + ); -if (environment.isDev()) { - //TODO create self-signed for localhost - const port = 3000; - app.listen(port, () => { - console.log(`Server is running at http://localhost:${port}`); - }); -} else { - //TODO start with HTTPS in production + if (environment.isDev()) { + //TODO create self-signed for localhost + const port = 3000; + app.listen(port, () => { + console.log(`Server is running at http://localhost:${port}`); + }); + } else { + //TODO start with HTTPS in production + const port = 5443; + const SSL_CERT_PATH = process.env["SSL_CERT_PATH"]; + const SSL_KEY_PATH = process.env["SSL_KEY_PATH"]; + const SSL_KEY_PASSWORD = process.env["SSL_KEY_PASSWORD"]; + if (!SSL_CERT_PATH || !SSL_KEY_PATH || !SSL_KEY_PASSWORD) { + throw new Error("Missing SSL configuration"); + } + + const sslOptions: https.ServerOptions = { + cert: fs.readFileSync(SSL_CERT_PATH), + key: fs.readFileSync(SSL_KEY_PATH), + passphrase: SSL_KEY_PASSWORD, + minVersion: "TLSv1.3", + maxVersion: "TLSv1.3", + ecdhCurve: "X25519:prime256v1:secp384r1", + honorCipherOrder: false, + secureOptions: + constants.SSL_OP_NO_TLSv1 | + constants.SSL_OP_NO_TLSv1_1 | + constants.SSL_OP_NO_TLSv1_2 | + constants.SSL_OP_NO_SSLv2 | + constants.SSL_OP_NO_SSLv3, + }; + https.createServer(sslOptions, app).listen(port, () => { + console.log(`Server is running on port ${port}`); + }); + + // const h2SslOptions={}; + // http2.createServer(h2SslOptions, app).listen(port, ()=>{ + // console.log(`HTTP/2 server is running on port ${port}`); + // }) + } + + console.log("done"); } -console.log("done"); +run(); diff --git a/packages/api/src/middleware/authentication.ts b/packages/api/src/middleware/authentication.ts index 03b3326..13a8f36 100644 --- a/packages/api/src/middleware/authentication.ts +++ b/packages/api/src/middleware/authentication.ts @@ -1,39 +1,30 @@ -import { type Request, type RequestHandler } from "express"; +import { type Request, type Response } from "express"; -import { KC_SECURITY_NAME, keycloak } from "../services/keycloak/user/index.js"; +import { inject } from "../infrastructure/di/index.js"; +import { + KC_SECURITY_NAME, + KeycloakUser, +} from "../services/keycloak/user/index.js"; -function promisifyMiddleware( - request: Request, - middleware: RequestHandler, - response?: Response, -): Promise { - return new Promise((resolve, reject) => { - middleware(request, response || ({} as any), (next_result: any) => { - console.dir(next_result); - next_result instanceof Error - ? reject(next_result) - : resolve(next_result); - }); - }); -} +const keycloak = inject(KeycloakUser); async function doKeycloakAuth( request: Request, roles?: string[], response?: Response, ): Promise { - if (roles && roles.length > 0) { - const grant = await keycloak.getGrant(request, response ?? ({} as any)); - const allowed = roles.every(role => - grant.access_token?.hasRealmRole(role), - ); - if (allowed) { - return Promise.resolve(); - } else { - return Promise.reject(httpError(403, "Forbidden")); - } + if (!roles || roles.length < 1) { + roles = ["default-roles-taekwondo"]; + } + const allowed = await keycloak.checkRoles( + roles, + request, + response ?? ({} as any), + ); + if (allowed) { + return Promise.resolve(); } else { - return await promisifyMiddleware(request, keycloak.protect(), response); + return Promise.reject(httpError(403, "Forbidden")); } } diff --git a/packages/api/src/services/db/prisma.ts b/packages/api/src/services/db/prisma.ts index a395231..0633ce6 100644 --- a/packages/api/src/services/db/prisma.ts +++ b/packages/api/src/services/db/prisma.ts @@ -44,9 +44,9 @@ export class DatabaseService { private readonly prismaClient: ExtendedClient; public constructor() { - const dbUser = process.env["TKD_DB_USER"]; - const dbPassword = process.env["TKD_DB_PASSWORD"]; - const dbHost = process.env["TKD_DB_HOST"]; + const dbUser = process.env["TKD_DB_USER"]!; + const dbPassword = process.env["TKD_DB_PASSWORD"]!; + const dbHost = process.env["TKD_DB_HOST"]!; const connectionString = `postgresql://${dbUser}:${dbPassword}@${dbHost}:5432/taekwondo?schema=public`; diff --git a/packages/api/src/services/keycloak/admin/keycloak-admin.ts b/packages/api/src/services/keycloak/admin/keycloak-admin.ts index 859e4d4..7164c9e 100644 --- a/packages/api/src/services/keycloak/admin/keycloak-admin.ts +++ b/packages/api/src/services/keycloak/admin/keycloak-admin.ts @@ -1,23 +1,43 @@ import { Injectable } from "../../../infrastructure/di/injectable-decorator.js"; -import kcClientConfig from "./nodejs-api.json" with { type: "json" }; import KeycloakAdminClient from "@keycloak/keycloak-admin-client"; import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation.js"; import { Claim } from "../../../dtos/claim.js"; import { type User } from "../../../dtos/user.js"; +import { inject } from "../../../infrastructure/di/index.js"; +import { EnvironmentService } from "../../environmentService.js"; @Injectable() export class KeycloakAdmin { private readonly kcAdminClient: Promise; + private readonly environment = inject(EnvironmentService); + private readonly config = this.getConfig(); + + private async getConfig() { + if (this.environment.isDev()) { + const config = await import("./nodejs-api.dev.json", { + with: { type: "json" }, + }); + return config.default; + } else { + const config = await import("./nodejs-api.prod.json", { + with: { type: "json" }, + }); + return config.default; + } + } + constructor() { this.kcAdminClient = new Promise(async resolve => { + const config = await this.config; const client = new KeycloakAdminClient({ - baseUrl: kcClientConfig.baseUrl, + baseUrl: config["auth-server-url"], realmName: "taekwondo", }); await client.auth({ - ...kcClientConfig, + clientSecret: config.credentials.secret, + clientId: config.resource, grantType: "client_credentials", }); resolve(client); diff --git a/packages/api/src/services/keycloak/admin/nodejs-api.dev.json b/packages/api/src/services/keycloak/admin/nodejs-api.dev.json new file mode 100644 index 0000000..8f12f31 --- /dev/null +++ b/packages/api/src/services/keycloak/admin/nodejs-api.dev.json @@ -0,0 +1,10 @@ +{ + "realm": "taekwondo", + "auth-server-url": "http://localhost:8080/", + "ssl-required": "external", + "resource": "nodejs-api", + "credentials": { + "secret": "devsecret" + }, + "confidential-port": 0 +} diff --git a/packages/api/src/services/keycloak/admin/nodejs-api.json b/packages/api/src/services/keycloak/admin/nodejs-api.json deleted file mode 100644 index 88f0ce4..0000000 --- a/packages/api/src/services/keycloak/admin/nodejs-api.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "clientId": "nodejs-api", - "name": "NodejsApi", - "description": "", - "rootUrl": "http://localhost:3000", - "adminUrl": "http://localhost:3000", - "baseUrl": "http://localhost:3000", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "devsecret", - "redirectUris": ["http://localhost:3000/*"], - "webOrigins": ["http://localhost:3000"], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": false, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": true, - "publicClient": false, - "frontchannelLogout": true, - "protocol": "openid-connect", - "attributes": { - "realm_client": "false", - "oidc.ciba.grant.enabled": "false", - "client.secret.creation.time": "1762463686", - "backchannel.logout.session.required": "true", - "standard.token.exchange.enabled": "true", - "oauth2.device.authorization.grant.enabled": "false", - "pkce.code.challenge.method": "S256", - "backchannel.logout.revoke.offline.tokens": "false", - "dpop.bound.access.tokens": "false" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": -1, - "defaultClientScopes": [ - "service_account", - "web-origins", - "acr", - "roles", - "profile", - "basic", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "organization", - "offline_access", - "microprofile-jwt" - ], - "access": { - "view": true, - "configure": true, - "manage": true - } -} diff --git a/packages/api/src/services/keycloak/user/keycloak-user.ts b/packages/api/src/services/keycloak/user/keycloak-user.ts index c5f9439..4edb9b8 100644 --- a/packages/api/src/services/keycloak/user/keycloak-user.ts +++ b/packages/api/src/services/keycloak/user/keycloak-user.ts @@ -1,11 +1,52 @@ +import { type Request, type RequestHandler, type Response } from "express"; import KeycloakConnect from "keycloak-connect"; +import { inject } from "../../../infrastructure/di/index.js"; +import { Injectable } from "../../../infrastructure/di/injectable-decorator.js"; import { sessionStore } from "../../../infrastructure/sessionHandler.js"; -import keycloakConfig from "./keycloak.json" with { type: "json" }; +import { EnvironmentService } from "../../environmentService.js"; +//TODO transform into service and load config based on environment (see keycloak-admin) export const KC_SECURITY_NAME = "bearerAuth"; -export const keycloak = new KeycloakConnect( - { store: sessionStore }, - keycloakConfig, -); -export const kcUserConfig = keycloakConfig; +@Injectable() +export class KeycloakUser { + private readonly environment = inject(EnvironmentService); + private readonly keycloakConnect: Promise; + + private async getConfig() { + if (this.environment.isDev()) { + const config = await import("./keycloak.dev.json", { + with: { type: "json" }, + }); + return config.default; + } else { + const config = await import("./keycloak.prod.json", { + with: { type: "json" }, + }); + return config.default; + } + } + + public constructor() { + this.keycloakConnect = new Promise(async resolve => { + const config = await this.getConfig(); + const client = new KeycloakConnect({ store: sessionStore }, config); + resolve(client); + }); + } + + public async checkRoles( + roles: string[], + request: Request, + response: Response, + ) { + const client = await this.keycloakConnect; + const grant = await client.getGrant(request, response ?? ({} as any)); + return roles.every(role => grant.access_token?.hasRealmRole(role)); + } + + public async middleware(): Promise { + const client = await this.keycloakConnect; + return client.middleware(); + } +} diff --git a/packages/api/src/services/keycloak/user/keycloak.json b/packages/api/src/services/keycloak/user/keycloak.dev.json similarity index 100% rename from packages/api/src/services/keycloak/user/keycloak.json rename to packages/api/src/services/keycloak/user/keycloak.dev.json diff --git a/packages/api/src/services/membersService.ts b/packages/api/src/services/membersService.ts index 9705eff..62c4cd8 100644 --- a/packages/api/src/services/membersService.ts +++ b/packages/api/src/services/membersService.ts @@ -1,7 +1,7 @@ import express from "express"; import { - type Member, type MemberCreateArgs, + type MemberDto, mapDbMemberToMember, } from "../dtos/member.js"; import { inject, Injectable } from "../infrastructure/di/index.js"; @@ -12,7 +12,7 @@ import { DatabaseService } from "./db/prisma.js"; export class MembersService { private readonly database = inject(DatabaseService); - public async getMembers(request: express.Request): Promise { + public async getMembers(request: express.Request): Promise { const dbMembers = await this.database.doRequest( async prisma => await prisma.member.findMany(), request, @@ -23,7 +23,7 @@ export class MembersService { public async getMemberById( memberId: string, request: express.Request, - ): Promise { + ): Promise { const member = await this.database.doRequest( async prisma => await prisma.member.findFirst({ where: { id: memberId } }), @@ -42,11 +42,11 @@ export class MembersService { public async createMember( createArgs: MemberCreateArgs, request: express.Request, - ): Promise { - const x: MemberCreateArgs = { - ...createArgs, - geburtsdatum: createArgs.geburtsdatum ?? null, - }; + ): Promise { + // const x: MemberCreateArgs = { + // ...createArgs, + // geburtsdatum: createArgs.geburtsdatum ?? null, + // }; const newMember = await this.database.doRequest( async prisma => await prisma.member.create({ data: createArgs }), request, @@ -55,15 +55,16 @@ export class MembersService { } public async updateMember( - member: Member, + member: MemberDto, request: express.Request, - ): Promise { + ): Promise { const id = member.id; if (!id) { throw new Error("Member id is required for update", { cause: ErrorStates.BAD_REQUEST, }); } + const modifiedMember = await this.database.doRequest( async prisma => await prisma.member.update({ diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index e2903f9..0c96862 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -56,5 +56,6 @@ "alwaysStrict": true, "forceConsistentCasingInFileNames": true }, - "files": ["./src/index.ts"] + "files": ["./src/index.ts"], + "include": ["src"] } diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 3f30f3c..c5f4111 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -7,7 +7,8 @@ "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", - "generate": "oazapfts --optimistic --useEnumType ./src/app/generated-api/swagger.json ./src/app/generated-api/api.ts" + "generate": "oazapfts --optimistic --useEnumType ./src/app/generated-api/swagger.json ./src/app/generated-api/api.ts", + "dev": "pnpm -w run dev" }, "prettier": { "printWidth": 100, @@ -29,7 +30,7 @@ "@angular/core": "^20.3.15", "@angular/forms": "^20.3.15", "@angular/material": "20.2.11", - "@angular/material-date-fns-adapter": "^21.0.2", + "@angular/material-date-fns-adapter": "^20.2.14", "@angular/platform-browser": "^20.3.15", "@angular/router": "^20.3.15", "@date-fns/tz": "^1.4.1", @@ -57,4 +58,4 @@ "oazapfts": "^6.3.1", "typescript": "~5.9.3" } -} \ No newline at end of file +} diff --git a/packages/frontend/src/app/app.config.ts b/packages/frontend/src/app/app.config.ts index 724943e..28be310 100644 --- a/packages/frontend/src/app/app.config.ts +++ b/packages/frontend/src/app/app.config.ts @@ -1,6 +1,8 @@ import { ApplicationConfig, + isDevMode, LOCALE_ID, + provideAppInitializer, provideBrowserGlobalErrorListeners, provideZoneChangeDetection, } from '@angular/core'; @@ -33,9 +35,11 @@ import { import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; import { de } from 'date-fns/locale'; import { routes } from './app.routes'; +import { initializeApi } from './util/api'; export const appConfig: ApplicationConfig = { providers: [ + provideAppInitializer(initializeApi), provideRouter( routes, withPreloading(PreloadAllModules), @@ -45,7 +49,7 @@ export const appConfig: ApplicationConfig = { provideZoneChangeDetection({ eventCoalescing: true }), provideKeycloak({ config: { - url: true + url: isDevMode() ? 'http://localhost:8080/' : 'https://keycloak.toni714.de:8443/', realm: 'taekwondo', @@ -55,6 +59,9 @@ export const appConfig: ApplicationConfig = { onLoad: 'check-sso', silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html', + pkceMethod: 'S256', + flow: 'standard', + locale: 'de', }, features: [withAutoRefreshToken({ sessionTimeout: 600000 })], providers: [AutoRefreshTokenService, UserActivityService], @@ -63,7 +70,7 @@ export const appConfig: ApplicationConfig = { provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG, useValue: [ createInterceptorCondition({ - urlPattern: true + urlPattern: isDevMode() ? /^http:\/\/localhost:(3000|8080)\/.*$/ : /^https:\/\/tkd-api\.toni714\.de\/.*$/, }), diff --git a/packages/frontend/src/app/components/delete-member-dialog/delete-member-dialog.component.ts b/packages/frontend/src/app/components/delete-member-dialog/delete-member-dialog.component.ts index e078ca4..f92fbd3 100644 --- a/packages/frontend/src/app/components/delete-member-dialog/delete-member-dialog.component.ts +++ b/packages/frontend/src/app/components/delete-member-dialog/delete-member-dialog.component.ts @@ -14,9 +14,13 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import * as api from '../../generated-api/api'; -import { DatePipePipe } from '../../pipes/date-pipe.pipe.js'; +import { DatePipePipe, dateToLocal } from '../../pipes/date-pipe.pipe.js'; import { EditMemberDialogComponent } from '../edit-member-dialog/edit-member-dialog.component.js'; +type DeletionForm = { + kuendigungzum: FormControl; +}; + @Component({ selector: 'app-delete-member-dialog', standalone: true, @@ -35,23 +39,27 @@ import { EditMemberDialogComponent } from '../edit-member-dialog/edit-member-dia styleUrl: './delete-member-dialog.component.scss', }) export class DeleteMemberDialogComponent { - form: FormGroup; - member: api.Member; + form: FormGroup; + member: api.MemberDto; constructor( - @Inject(MAT_DIALOG_DATA) public data: { member: api.Member }, + @Inject(MAT_DIALOG_DATA) public data: { member: api.MemberDto }, private dialogRef: MatDialogRef, ) { this.member = data.member; - this.form = new FormGroup({ - kuendigungzum: new FormControl('', [Validators.required]), + this.form = new FormGroup({ + kuendigungzum: new FormControl(null, [Validators.required]), }); } async submit() { - await api.scheduleDeletion( - this.member.id!, - this.form.controls['kuendigungzum'].value, - ); - this.dialogRef.close(); + const date = this.form.controls.kuendigungzum.value; + if (date) { + await api.scheduleDeletion( + this.member.id!, + dateToLocal(date).toISOString(), + ); + + this.dialogRef.close(); + } } } diff --git a/packages/frontend/src/app/components/edit-member-dialog/edit-member-dialog.component.ts b/packages/frontend/src/app/components/edit-member-dialog/edit-member-dialog.component.ts index 2d0b8e4..277532e 100644 --- a/packages/frontend/src/app/components/edit-member-dialog/edit-member-dialog.component.ts +++ b/packages/frontend/src/app/components/edit-member-dialog/edit-member-dialog.component.ts @@ -30,7 +30,7 @@ import { MemberDtoHelper } from '../../util/member.js'; export class EditMemberDialogComponent { form: FormGroup; constructor( - @Inject(MAT_DIALOG_DATA) public data: { member: api.Member }, + @Inject(MAT_DIALOG_DATA) public data: { member: api.MemberDto }, private memberDtoHelper: MemberDtoHelper, private dialogRef: MatDialogRef, ) { @@ -38,7 +38,7 @@ export class EditMemberDialogComponent { } submit() { - const member: api.Member = this.memberDtoHelper.memberFromForm( + const member: api.MemberDto = this.memberDtoHelper.memberFromForm( this.form, ); api.updateMember(member); diff --git a/packages/frontend/src/app/components/new-member/new-member.component.html b/packages/frontend/src/app/components/new-member/new-member.component.html index d5edacb..a89fec7 100644 --- a/packages/frontend/src/app/components/new-member/new-member.component.html +++ b/packages/frontend/src/app/components/new-member/new-member.component.html @@ -1,39 +1,43 @@

Neues Mitglied anlegen

- - Vorname - - - - Nachname - - - - Geschlecht - - - - Geburtsdatum - - DD.MM.YYYY - - - - - Geburtsort - - - Unterrichtsvertrag Abgegeben? - Verein Beigetreten? - - Passnummer (falls vorhanden) - - - - Kontakt - - - + + Vorname + + + + Nachname + + + + Geschlecht + + + + Geburtsdatum + + DD.MM.YYYY + + + + + Geburtsort + + + Verein Beigetreten? + + Passnummer (falls vorhanden) + + + + Kontakt + + +
diff --git a/packages/frontend/src/app/components/new-member/new-member.component.ts b/packages/frontend/src/app/components/new-member/new-member.component.ts index 48db2bb..52372ca 100644 --- a/packages/frontend/src/app/components/new-member/new-member.component.ts +++ b/packages/frontend/src/app/components/new-member/new-member.component.ts @@ -34,7 +34,7 @@ export class NewMemberComponent { ); onSubmit() { - const newMember: api.PickMemberExcludeKeyofMemberIdOrLoeschenam = + const newMember: api.MemberCreateArgs = this.memberDtoHelper.newMemberFromForm(this.memberForm); api.createMember(newMember) diff --git a/packages/frontend/src/app/components/view-members/view-members.html b/packages/frontend/src/app/components/view-members/view-members.html index d61bf1b..838423d 100644 --- a/packages/frontend/src/app/components/view-members/view-members.html +++ b/packages/frontend/src/app/components/view-members/view-members.html @@ -1,125 +1,142 @@

Mitglieder & Sportlerübersicht

-
- - - -
-
- - - - - + + + + +
Bearb. - -
+ + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - + + - - + + - - + + - - + + -
Bearb. + + Entf. - - Entf. + + Vorname - - Vorname + + Nachname - - Nachname + + Geschlecht{{ element.geschlecht }}Geschlecht{{ element.geschlecht }} Passnummer - {{ element.passnummer | nobreakHyphen }} - Passnummer + {{ element.passnummer | nobreakHyphen }} + Geburtsdatum - {{ element.geburtsdatum | datePipe }} - Geburtsdatum + {{ element.geburtsdatum | datePipe }} + Geburtsort{{ element.geburtsort }}Unterrichtsvertrag - - - Geburtsort{{ element.geburtsort }} Vereinsmitgliedschaft - - - + Vereinsmitgliedschaft + + @if(element.verein){ + + }@else { + + + } + Kontakt{{ element.kontakt }}Kontakt{{ element.kontakt }} Kündigung zum - {{ (element.kuendigungzum | datePipe) ?? "-" }} - + Kündigung zum + + {{ (element.kuendigungzum | datePipe) ?? "-" }} +
-
+ diff --git a/packages/frontend/src/app/components/view-members/view-members.scss b/packages/frontend/src/app/components/view-members/view-members.scss index e69de29..38b8b06 100644 --- a/packages/frontend/src/app/components/view-members/view-members.scss +++ b/packages/frontend/src/app/components/view-members/view-members.scss @@ -0,0 +1,21 @@ +mat-radio-group { + display: flex; + flex-direction: column; +} + +.filter-header { + display: flex; + flex-direction: row; + justify-content: space-evenly; +} + +mat-form-field { + height: 100%; +} + +.status-filters { + display: flex; + flex-direction: row; + justify-content: center; + gap: 1rem; +} diff --git a/packages/frontend/src/app/components/view-members/view-members.ts b/packages/frontend/src/app/components/view-members/view-members.ts index 2791556..ff7ece4 100644 --- a/packages/frontend/src/app/components/view-members/view-members.ts +++ b/packages/frontend/src/app/components/view-members/view-members.ts @@ -1,13 +1,18 @@ -import { AsyncPipe, NgIf } from '@angular/common'; -import { Component } from '@angular/core'; +import { AfterViewInit, Component, viewChild } from '@angular/core'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; +import { MatCheckbox } from '@angular/material/checkbox'; +import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialog } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; +import { MatRadioModule } from '@angular/material/radio'; +import { MatSort, MatSortModule } from '@angular/material/sort'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; -import { BehaviorSubject, firstValueFrom, from, Observable } from 'rxjs'; +import { combineLatest, startWith } from 'rxjs'; import * as api from '../../generated-api/api'; import { DatePipePipe } from '../../pipes/date-pipe.pipe.js'; import { NobreakHyphenPipe } from '../../pipes/nobreak-hyphen.pipe.js'; @@ -16,6 +21,12 @@ import { DeleteMemberDialogComponent } from '../delete-member-dialog/delete-memb import { EditMemberDialogComponent } from '../edit-member-dialog/edit-member-dialog.component.js'; import { NewMemberComponent } from '../new-member/new-member.component'; +enum MemberStatusFilter { + ACTIVE = 'ACTIVE', + INACTIVE = 'INACTIVE', + ALL = 'ALL', +} + @Component({ selector: 'app-view-members', imports: [ @@ -23,60 +34,33 @@ import { NewMemberComponent } from '../new-member/new-member.component'; MatFormFieldModule, MatInputModule, MatIconModule, - NgIf, StylizeNamesPipe, NobreakHyphenPipe, RouterModule, MatButtonModule, - AsyncPipe, DatePipePipe, + ReactiveFormsModule, + MatDatepickerModule, + MatTooltipModule, + MatCheckbox, + MatRadioModule, + MatSortModule, ], templateUrl: './view-members.html', styleUrl: './view-members.scss', }) -export class ViewMembers { - async deleteMember(memberId: any) { - const dataSource = await firstValueFrom(this.dataSource$); +export class ViewMembers implements AfterViewInit { + protected MemberStatusFilter = MemberStatusFilter; + protected readonly filterControl = new FormControl(''); + protected readonly statusControl = new FormControl( + MemberStatusFilter.ACTIVE, + ); + protected readonly statusDateControl = new FormControl(null); + protected readonly onlyWithID = new FormControl(false); + protected dataSource = new MatTableDataSource(); + private readonly datePipe = new DatePipePipe(); - const dialogRef = this.dialog.open(DeleteMemberDialogComponent, { - data: { - member: dataSource.data.find(member => member.id === memberId), - }, - hasBackdrop: true, - width: '90%', - height: '90%', - }); - - dialogRef.afterClosed().subscribe(async result => { - this.dataSource$ = this.updateData(); - }); - } - newMember() { - const dialogRef = this.dialog.open(NewMemberComponent, {}); - dialogRef.afterClosed().subscribe(() => { - this.dataSource$ = this.updateData(); - }); - } - async editMember(memberId: any) { - const dataSource = await firstValueFrom(this.dataSource$); - - const dialogRef = this.dialog.open(EditMemberDialogComponent, { - data: { - member: dataSource.data.find(member => member.id === memberId), - }, - hasBackdrop: true, - width: '90%', - height: '90%', - }); - - dialogRef.afterClosed().subscribe(async result => { - this.dataSource$ = this.updateData(); - }); - } - dataSource$: Observable> = - new BehaviorSubject(new MatTableDataSource([] as api.Member[])); - filter$: BehaviorSubject = new BehaviorSubject(''); - displayedColumns: string[] = [ + protected readonly displayedColumns: string[] = [ 'edit', 'delete', 'vorname', @@ -85,31 +69,141 @@ export class ViewMembers { 'passnummer', 'geburtsdatum', 'geburtsort', - 'uvertrag', 'verein', 'kontakt', 'kuendigungzum', ]; - applyFilter(event: KeyboardEvent) { - let filterValue = (event.target as HTMLInputElement).value ?? ''; - filterValue = filterValue.trim(); - filterValue = filterValue.toLowerCase(); - this.filter$.next(filterValue); + protected deleteMember(memberId: any) { + const dialogRef = this.dialog.open(DeleteMemberDialogComponent, { + data: { + member: this.dataSource.data.find( + member => member.id === memberId, + ), + }, + hasBackdrop: true, + width: '90%', + height: '90%', + }); + + dialogRef.afterClosed().subscribe(async result => { + this.updateData(); + }); } - constructor(private dialog: MatDialog) { - this.dataSource$ = this.updateData(); + protected newMember() { + const dialogRef = this.dialog.open(NewMemberComponent, {}); + dialogRef.afterClosed().subscribe(() => { + this.updateData(); + }); + } + protected editMember(memberId: any) { + const dialogRef = this.dialog.open(EditMemberDialogComponent, { + data: { + member: this.dataSource.data.find( + member => member.id === memberId, + ), + }, + hasBackdrop: true, + width: '90%', + height: '90%', + }); + + dialogRef.afterClosed().subscribe(async result => { + //TODO do upsert instead + this.updateData(); + }); } - updateData() { - return from( - api.getMembers().then(members => { - let dataSource = new MatTableDataSource(members); - this.filter$.subscribe(filter => { - dataSource.filter = filter; - }); - return dataSource; - }), - ); + public constructor(private dialog: MatDialog) { + const originalFilterPredicate = this.dataSource.filterPredicate; + this.dataSource.filterPredicate = ( + data: api.MemberDto, + encodedFilter: string, + ) => { + const parts = encodedFilter.split(';'); + const filter = parts[0]; + const status = parts[1] as MemberStatusFilter; + const statusDateStr = parts[2]; + const onlyWithID = parts[3] === 'true'; + + const currentDate = + statusDateStr && statusDateStr != '' + ? new Date(statusDateStr) + : null; + const cancellationDate = data.kuendigungzum + ? new Date(data.kuendigungzum) + : null; + if ( + currentDate && + cancellationDate && + ((status === MemberStatusFilter.ACTIVE && + cancellationDate <= currentDate) || + (status === MemberStatusFilter.INACTIVE && + cancellationDate > currentDate)) + ) { + return false; + } + if (status === MemberStatusFilter.INACTIVE && !cancellationDate) { + return false; + } + + if ( + onlyWithID && + (!data.passnummer || data.passnummer.trim() === '') + ) { + return false; + } + + return originalFilterPredicate( + { + ...data, + geburtsdatum: this.datePipe.transform( + data.geburtsdatum ?? undefined, + ), + kuendigungzum: this.datePipe.transform( + data.kuendigungzum ?? undefined, + ), + letztePruefung: this.datePipe.transform( + data.letztePruefung ?? undefined, + ), + }, + filter, + ); + }; + combineLatest([ + this.filterControl.valueChanges.pipe( + startWith(this.filterControl.value), + ), + this.statusControl.valueChanges.pipe( + startWith(this.statusControl.value), + ), + this.statusDateControl.valueChanges.pipe( + startWith(this.statusDateControl.value), + ), + this.onlyWithID.valueChanges.pipe(startWith(this.onlyWithID.value)), + ]) + // .pipe(takeUntilDestroyed()) + .subscribe(combined => { + const filter = combined[0]; + const status = combined[1]; + const date = combined[2]; + const onlyWithID = combined[3]; + this.dataSource.filter = `${filter?.replaceAll(';', '') ?? ''};${status};${date?.toISOString()};${onlyWithID}`; + console.log('filter', this.dataSource.filter); + }); + + this.updateData(); + } + + protected readonly sort = viewChild.required(MatSort); + public ngAfterViewInit(): void { + this.dataSource.sort = this.sort(); + } + + private async updateData(): Promise { + const members = await api.getMembers(); + console.log('Members loaded'); + console.dir(members); + this.dataSource.data = members; } } diff --git a/packages/frontend/src/app/pipes/date-pipe.pipe.ts b/packages/frontend/src/app/pipes/date-pipe.pipe.ts index 90324b4..d3ddc5e 100644 --- a/packages/frontend/src/app/pipes/date-pipe.pipe.ts +++ b/packages/frontend/src/app/pipes/date-pipe.pipe.ts @@ -1,20 +1,28 @@ -import { Inject, Pipe, PipeTransform } from '@angular/core'; +import { inject, Pipe, PipeTransform } from '@angular/core'; import { MAT_DATE_LOCALE } from '@angular/material/core'; import * as tz from '@date-fns/tz'; import { format, Locale } from 'date-fns'; @Pipe({ - name: 'datePipe', - standalone: true + name: 'datePipe', + standalone: true, }) export class DatePipePipe implements PipeTransform { - constructor(@Inject(MAT_DATE_LOCALE) private locale: Locale){} + private readonly locale = inject(MAT_DATE_LOCALE); - transform(value?: string): string { - if(!value) return "-"; - const date=new tz.TZDate(value!, "UTC"); - const timeZone=Intl.DateTimeFormat().resolvedOptions().timeZone; - const zonedDate=date.withTimeZone(timeZone); - return format(zonedDate, 'P', { locale: this.locale }); - } + public transform(value?: string): string { + if (!value) return '-'; + const zonedDate = dateToUTC(value); + return format(zonedDate, 'P', { locale: this.locale }); + } +} + +export function dateToLocal(date: Date): Date { + return new Date(date.getTime() - date.getTimezoneOffset() * 60000); +} + +export function dateToUTC(value: string): Date { + const date = new tz.TZDate(value!, 'UTC'); + const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + return date.withTimeZone(timeZone); } diff --git a/packages/frontend/src/app/services/authentication.ts b/packages/frontend/src/app/services/authentication.ts index df1e20b..560740d 100644 --- a/packages/frontend/src/app/services/authentication.ts +++ b/packages/frontend/src/app/services/authentication.ts @@ -116,6 +116,7 @@ export class Authentication { return await this.keycloak.login({ redirectUri: redirectUri, locale: 'de-DE', + //TODO loginHint:'' }); } diff --git a/packages/frontend/src/app/util/api.ts b/packages/frontend/src/app/util/api.ts new file mode 100644 index 0000000..ce5a437 --- /dev/null +++ b/packages/frontend/src/app/util/api.ts @@ -0,0 +1,10 @@ +import { isDevMode } from '@angular/core'; + +import * as api from '../generated-api/api'; + +export function initializeApi() { + if (isDevMode()) { + // override api url in dev mode + api.defaults.baseUrl = 'http://localhost:3000'; + } +} diff --git a/packages/frontend/src/app/util/member.ts b/packages/frontend/src/app/util/member.ts index d54c6ab..a18714f 100644 --- a/packages/frontend/src/app/util/member.ts +++ b/packages/frontend/src/app/util/member.ts @@ -2,10 +2,8 @@ import { Inject, Injectable } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { MAT_DATE_LOCALE } from '@angular/material/core'; import { Locale, parse } from 'date-fns'; -import { - Member, - PickMemberExcludeKeyofMemberIdOrLoeschenam, -} from '../generated-api/api'; +import { MemberCreateArgs, MemberDto } from '../generated-api/api'; +import { dateToLocal, dateToUTC } from '../pipes/date-pipe.pipe'; interface MemberForm { id: FormControl; @@ -15,12 +13,12 @@ interface MemberForm { geburtsdatum: FormControl; geburtsort: FormControl; passnummer: FormControl; - uvertrag: FormControl; verein: FormControl; kontakt: FormControl; graduierung: FormControl; - kuendigungzum: FormControl; - letztePruefung: FormControl; + kuendigungzum: FormControl; + letztePruefung: FormControl; + marker: FormControl; } @Injectable({ @@ -28,7 +26,7 @@ interface MemberForm { }) export class MemberDtoHelper { constructor(@Inject(MAT_DATE_LOCALE) private locale: Locale) {} - public newMemberForm(member?: Member): FormGroup { + public newMemberForm(member?: MemberDto): FormGroup { const geburtsdatum = member?.geburtsdatum; let enDate: Date | null = null; if (geburtsdatum) { @@ -38,62 +36,71 @@ export class MemberDtoHelper { }); } - return new FormGroup({ + return new FormGroup({ id: new FormControl(member?.id), vorname: new FormControl(member?.vorname), nachname: new FormControl(member?.nachname), geschlecht: new FormControl(member?.geschlecht), - geburtsdatum: new FormControl(enDate), + geburtsdatum: new FormControl( + member?.geburtsdatum ? dateToUTC(member?.geburtsdatum) : null, + ), geburtsort: new FormControl(member?.geburtsort), passnummer: new FormControl(member?.passnummer), - uvertrag: new FormControl(member?.uvertrag), verein: new FormControl(member?.verein), kontakt: new FormControl(member?.kontakt), graduierung: new FormControl(member?.graduierung), - kuendigungzum: new FormControl(member?.kuendigungzum), - letztePruefung: new FormControl(member?.letztePruefung), + kuendigungzum: new FormControl( + member?.kuendigungzum ? dateToUTC(member?.kuendigungzum) : null, + ), + letztePruefung: new FormControl( + member?.letztePruefung + ? dateToUTC(member?.letztePruefung) + : null, + ), + marker: new FormControl(false), }); } - memberFromForm(form: FormGroup): Member { + memberFromForm(form: FormGroup): MemberDto { return { id: form.controls.id.value ?? null, vorname: form.controls.vorname.value ?? null, nachname: form.controls.nachname.value ?? null, geschlecht: form.controls.geschlecht.value ?? null, geburtsdatum: form.controls.geburtsdatum.value - ? JSON.stringify(form.controls.geburtsdatum.value) + ? dateToLocal(form.controls.geburtsdatum.value).toISOString() : null, geburtsort: form.controls.geburtsort.value ?? null, passnummer: form.controls.passnummer.value ?? null, - uvertrag: form.controls.uvertrag.value ?? null, verein: form.controls.verein.value ?? null, kontakt: form.controls.kontakt.value ?? null, graduierung: form.controls.graduierung?.value ?? null, - kuendigungzum: form.controls.kuendigungzum.value ?? null, //TODO not for new member - letztePruefung: form.controls.letztePruefung?.value ?? null, //TODO not for new member - loeschenam: null, //TODO? not client settable + kuendigungzum: form.controls.kuendigungzum.value + ? dateToLocal(form.controls.kuendigungzum.value).toISOString() + : null, + letztePruefung: form.controls.letztePruefung.value + ? dateToLocal(form.controls.letztePruefung.value).toISOString() + : null, + marker: form.controls.marker.value ?? false, }; } - newMemberFromForm( - form: FormGroup, - ): PickMemberExcludeKeyofMemberIdOrLoeschenam { + newMemberFromForm(form: FormGroup): MemberCreateArgs { return { vorname: form.controls.vorname.value ?? null, nachname: form.controls.nachname.value ?? null, geschlecht: form.controls.geschlecht.value ?? null, geburtsdatum: form.controls.geburtsdatum.value - ? JSON.stringify(form.controls.geburtsdatum.value) + ? dateToLocal(form.controls.geburtsdatum.value).toISOString() : null, geburtsort: form.controls.geburtsort.value ?? null, passnummer: form.controls.passnummer.value ?? null, - uvertrag: form.controls.uvertrag.value ?? null, verein: form.controls.verein.value ?? null, kontakt: form.controls.kontakt.value ?? null, graduierung: form.controls.graduierung?.value ?? null, kuendigungzum: null, //TODO not for new member letztePruefung: null, //TODO not for new member + marker: form.controls.marker.value ?? false, }; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0bf26c7..f1c112e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + nodemon: + specifier: ^3.1.11 + version: 3.1.11 tsoa: specifier: ^6.6.0 version: 6.6.0 @@ -41,6 +44,9 @@ importers: express-session: specifier: ^1.18.2 version: 1.18.2 + helmet: + specifier: ^8.1.0 + version: 8.1.0 keycloak-connect: specifier: ^26.1.1 version: 26.1.1 @@ -50,6 +56,9 @@ importers: rxjs: specifier: ~7.8.2 version: 7.8.2 + tsoa: + specifier: ^6.6.0 + version: 6.6.0 devDependencies: '@types/connect-pg-simple': specifier: ^7.0.3 @@ -100,8 +109,8 @@ importers: specifier: 20.2.11 version: 20.2.11(437fd31793958c779574d25f524a2c50) '@angular/material-date-fns-adapter': - specifier: ^21.0.2 - version: 21.0.2(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/material@20.2.11(437fd31793958c779574d25f524a2c50))(date-fns@4.1.0) + specifier: ^20.2.14 + version: 20.2.14(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/material@20.2.11(437fd31793958c779574d25f524a2c50))(date-fns@4.1.0) '@angular/platform-browser': specifier: ^20.3.15 version: 20.3.15(@angular/common@20.3.15(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1)) @@ -414,11 +423,11 @@ packages: '@angular/platform-browser': 20.3.15 rxjs: ^6.5.3 || ^7.4.0 - '@angular/material-date-fns-adapter@21.0.2': - resolution: {integrity: sha512-vNJSiC6Ik6jmtdLUPWm514yJjoRP6blpw0DsACLcEDZJuIujAtvN/YmE3e+azk3Psr3nJgR3p029nQmKfHm7pw==} + '@angular/material-date-fns-adapter@20.2.14': + resolution: {integrity: sha512-/LRtKSP1WOSgbahbeQlagCZoMOQc9VYZqSDSzmhV0xfiNtT6oQD9a5KMbr5LkozJZQnZUoY7QYveTXe4BD8r2Q==} peerDependencies: - '@angular/core': ^21.0.0 || ^22.0.0 - '@angular/material': 21.0.2 + '@angular/core': ^20.0.0 || ^21.0.0 + '@angular/material': 20.2.14 date-fns: '>2.20.0 <5.0' '@angular/material@20.2.11': @@ -3418,10 +3427,6 @@ packages: glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} - hasBin: true - glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} hasBin: true @@ -3448,6 +3453,10 @@ packages: engines: {node: '>=0.4.7'} hasBin: true + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -3467,6 +3476,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + helmet@8.1.0: + resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==} + engines: {node: '>=18.0.0'} + hmac-drbg@1.0.1: resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} @@ -3565,6 +3578,9 @@ packages: peerDependencies: postcss: ^8.1.0 + ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + ignore-walk@8.0.0: resolution: {integrity: sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==} engines: {node: ^20.17.0 || >=22.9.0} @@ -4239,6 +4255,11 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + nodemon@3.1.11: + resolution: {integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==} + engines: {node: '>=10'} + hasBin: true + nopt@8.1.0: resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} engines: {node: ^18.17.0 || >=20.5.0} @@ -4632,6 +4653,9 @@ packages: prr@1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} + pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} @@ -4977,6 +5001,10 @@ packages: resolution: {integrity: sha512-ZpzWAFHIFqyFE56dXqgX/DkDRZdz+rRcjoIk/RQU4IX0wiCv1l8S7ZrXDHcCc+uaf+6o7w3h2l3g6GYG5TKN9Q==} engines: {node: ^18.17.0 || >=20.5.0} + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + slice-ansi@5.0.0: resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} engines: {node: '>=12'} @@ -5112,6 +5140,10 @@ packages: resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -5202,6 +5234,10 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -5269,6 +5305,9 @@ packages: resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} engines: {node: '>= 0.8'} + undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -5344,10 +5383,6 @@ packages: resolution: {integrity: sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==} engines: {node: ^18.17.0 || >=20.5.0} - validator@13.15.20: - resolution: {integrity: sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==} - engines: {node: '>= 0.10'} - validator@13.15.23: resolution: {integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==} engines: {node: '>= 0.10'} @@ -6011,7 +6046,7 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 - '@angular/material-date-fns-adapter@21.0.2(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/material@20.2.11(437fd31793958c779574d25f524a2c50))(date-fns@4.1.0)': + '@angular/material-date-fns-adapter@20.2.14(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/material@20.2.11(437fd31793958c779574d25f524a2c50))(date-fns@4.1.0)': dependencies: '@angular/core': 20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1) '@angular/material': 20.2.11(437fd31793958c779574d25f524a2c50) @@ -6082,7 +6117,7 @@ snapshots: '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 convert-source-map: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -6102,7 +6137,7 @@ snapshots: '@babel/types': 7.28.5 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -6162,7 +6197,7 @@ snapshots: '@babel/core': 7.28.3 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.11 transitivePeerDependencies: @@ -6759,7 +6794,7 @@ snapshots: '@babel/parser': 7.28.5 '@babel/template': 7.27.2 '@babel/types': 7.28.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -7840,13 +7875,13 @@ snapshots: '@tsoa/runtime': 6.6.0 '@types/multer': 1.4.13 fs-extra: 11.3.2 - glob: 10.4.5 + glob: 10.5.0 handlebars: 4.7.8 merge-anything: 5.1.7 minimatch: 9.0.5 ts-deepmerge: 7.0.3 typescript: 5.9.3 - validator: 13.15.20 + validator: 13.15.23 yaml: 2.8.1 yargs: 17.7.2 transitivePeerDependencies: @@ -8345,7 +8380,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) http-errors: 2.0.1 iconv-lite: 0.7.0 on-finished: 2.4.1 @@ -8697,9 +8732,11 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.3: + debug@4.4.3(supports-color@5.5.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 deep-is@0.1.4: optional: true @@ -9068,7 +9105,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 @@ -9099,7 +9136,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -9173,7 +9210,7 @@ snapshots: finalhandler@2.1.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -9193,7 +9230,7 @@ snapshots: follow-redirects@1.15.11(debug@4.4.3): optionalDependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) foreground-child@3.3.1: dependencies: @@ -9287,7 +9324,7 @@ snapshots: dependencies: basic-ftp: 5.0.5 data-uri-to-buffer: 6.0.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) transitivePeerDependencies: - supports-color optional: true @@ -9315,15 +9352,6 @@ snapshots: glob-to-regexp@0.4.1: {} - glob@10.4.5: - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - glob@10.5.0: dependencies: foreground-child: 3.3.1 @@ -9359,6 +9387,8 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 + has-flag@3.0.0: {} + has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -9376,6 +9406,8 @@ snapshots: dependencies: function-bind: 1.1.2 + helmet@8.1.0: {} + hmac-drbg@1.0.1: dependencies: hash.js: 1.1.7 @@ -9440,7 +9472,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -9459,7 +9491,7 @@ snapshots: http-proxy-middleware@3.0.5: dependencies: '@types/http-proxy': 1.17.17 - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) http-proxy: 1.18.1(debug@4.4.3) is-glob: 4.0.3 is-plain-object: 5.0.0 @@ -9482,7 +9514,7 @@ snapshots: https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -9504,6 +9536,8 @@ snapshots: dependencies: postcss: 8.5.6 + ignore-by-default@1.0.1: {} + ignore-walk@8.0.0: dependencies: minimatch: 10.1.1 @@ -9657,7 +9691,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -9905,7 +9939,7 @@ snapshots: log4js@6.9.1: dependencies: date-format: 4.0.14 - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) flatted: 3.3.3 rfdc: 1.4.1 streamroller: 3.1.5 @@ -10193,6 +10227,19 @@ snapshots: node-releases@2.0.27: {} + nodemon@3.1.11: + dependencies: + chokidar: 3.6.0 + debug: 4.4.3(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.7.3 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + nopt@8.1.0: dependencies: abbrev: 3.0.1 @@ -10376,7 +10423,7 @@ snapshots: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) get-uri: 6.0.5 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -10627,7 +10674,7 @@ snapshots: proxy-agent@6.5.0: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -10644,6 +10691,8 @@ snapshots: prr@1.0.1: optional: true + pstree.remy@1.1.8: {} + pump@3.0.3: dependencies: end-of-stream: 1.4.5 @@ -10828,7 +10877,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -10939,7 +10988,7 @@ snapshots: send@1.2.0: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -11074,6 +11123,10 @@ snapshots: transitivePeerDependencies: - supports-color + simple-update-notifier@2.0.0: + dependencies: + semver: 7.7.3 + slice-ansi@5.0.0: dependencies: ansi-styles: 6.2.3 @@ -11125,7 +11178,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) socks: 2.8.7 transitivePeerDependencies: - supports-color @@ -11168,7 +11221,7 @@ snapshots: spdy-transport@3.0.0: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -11179,7 +11232,7 @@ snapshots: spdy@4.0.2: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 @@ -11208,7 +11261,7 @@ snapshots: streamroller@3.1.5: dependencies: date-format: 4.0.14 - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) fs-extra: 8.1.0 transitivePeerDependencies: - supports-color @@ -11247,6 +11300,10 @@ snapshots: dependencies: ansi-regex: 6.2.2 + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -11352,6 +11409,8 @@ snapshots: toidentifier@1.0.1: {} + touch@3.1.1: {} + tr46@0.0.3: {} tree-dump@1.1.0(tslib@2.8.1): @@ -11381,7 +11440,7 @@ snapshots: tuf-js@3.1.0: dependencies: '@tufjs/models': 3.0.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@5.5.0) make-fetch-happen: 14.0.3 transitivePeerDependencies: - supports-color @@ -11412,6 +11471,8 @@ snapshots: dependencies: random-bytes: 1.0.0 + undefsafe@2.0.5: {} + undici-types@7.16.0: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -11464,8 +11525,6 @@ snapshots: validate-npm-package-name@6.0.2: {} - validator@13.15.20: {} - validator@13.15.23: {} vary@1.1.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c08c306..64fc45a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,7 +4,9 @@ packages: onlyBuiltDependencies: - '@parcel/watcher' + - '@prisma/engines' - chromedriver - esbuild - lmdb - msgpackr-extract + - prisma diff --git a/scripts/angular.ps1 b/scripts/angular.ps1 new file mode 100644 index 0000000..37558e1 --- /dev/null +++ b/scripts/angular.ps1 @@ -0,0 +1,2 @@ +Set-Location -Path $PSScriptRoot/../packages/frontend +pnpm ng serve diff --git a/scripts/api-ts.ps1 b/scripts/api-ts.ps1 new file mode 100644 index 0000000..d4de263 --- /dev/null +++ b/scripts/api-ts.ps1 @@ -0,0 +1,6 @@ +Set-Location -Path $PSScriptRoot/../packages/api +$env:TKD_DB_HOST="localhost" +$env:TKD_DB_USER="webapi" +$env:TKD_DB_PASSWORD="devpassword" +$env:TKD_COOKIE_SECRET="devsecret" +pnpx tsx --watch --tsconfig .\tsconfig.json .\src\index.ts diff --git a/scripts/apigen.ps1 b/scripts/apigen.ps1 new file mode 100644 index 0000000..74847cb --- /dev/null +++ b/scripts/apigen.ps1 @@ -0,0 +1,48 @@ +Write-Host "Generating initial Apidoc" +pnpx tsoa spec-and-routes +Write-Host "Generating initial Frontend API client" +pnpx oazapfts --optimistic --useEnumType packages/frontend/src/app/generated-api/swagger.json packages/frontend/src/app/generated-api/api.ts + +Write-Host "Starting watchers for API doc generation and Frontend client generation" +wt @( + "-w", + "0", + "-d", + ".", + "pwsh", + "-c", + "pnpm", + "nodemon", + "--watch", + "./packages/api/src/controllers", + "--watch", + "./packages/api/src/dtos", + "-e", + "ts", + "-x", + "pnpm", + "run", + "generate", + ";", + + "sp", + "--title", + "API Doc Gen", + "-d", + ".", + "pwsh", + "-c", + "pnpm", + "nodemon", + "--watch", + "./packages/frontend/src/app/generated-api/swagger.json", + "-e", + "json", + "-x", + "pnpm", + "--filter", + "frontend", + "run", + "generate" +) +Exit 0 diff --git a/scripts/dev.ps1 b/scripts/dev.ps1 new file mode 100644 index 0000000..fa1c903 --- /dev/null +++ b/scripts/dev.ps1 @@ -0,0 +1,44 @@ +$ErrorActionPreference = "Inquire" + +wt @( + "nt", + "-d", + ".", + "--title", + "Angular", + "pwsh", + "-NoExit", + "-File", + ".\scripts\angular.ps1", + ";", + + "nt", + "-d", + ".", + "--title", + "API TS", + "pwsh", + "-NoExit", + "-File", + ".\scripts\api-ts.ps1", + ";", + + "nt", + "-d", + ".", + "--title", + "Docker", + "pwsh", + "-NoExit", + "-File", + ".\scripts\docker.ps1", + ";", + + "nt", + "-d", + ".", + "--title", + "API Gen", + "pwsh", + "-File", + ".\scripts\apigen.ps1") diff --git a/scripts/docker.ps1 b/scripts/docker.ps1 new file mode 100644 index 0000000..3f595b0 --- /dev/null +++ b/scripts/docker.ps1 @@ -0,0 +1,2 @@ +Set-Location -Path $PSScriptRoot\..\packages\api\dev\ +docker compose up diff --git a/tsoa.json b/tsoa.json index 04d9734..01fb7c5 100644 --- a/tsoa.json +++ b/tsoa.json @@ -20,7 +20,7 @@ } }, "basePath": "", - "servers": ["localhost:3000", "tkd-api.toni714.de"], + "servers": ["tkd-api.toni714.de:443", "localhost:3000"], "info": { "title": "Tcc Member API", "description": "API for the TCC Member Database",