import legacy frontend & fix dependency issues

This commit is contained in:
tw-blue
2025-12-07 12:51:57 +01:00
parent 91ac875a32
commit cd6cfec7f3
52 changed files with 3963 additions and 267 deletions
+27
View File
@@ -0,0 +1,27 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}\\dist\\index.js",
"outFiles": [
"${workspaceFolder}/**/*.js"
],
"env": {
"TKD_DB_USER": "webapi",
"TKD_DB_PASSWORD": "devpassword",
"TKD_DB_HOST": "localhost",
"TKD_COOKIE_SECRET": "devsecret"
},
"runtimeExecutable": "C:\\Users\\Toni\\AppData\\Roaming\\fnm\\aliases\\default\\node"
}
]
}
File diff suppressed because it is too large Load Diff
@@ -21,3 +21,5 @@ CREATE DATABASE keycloak
-- replace Frontend redirect URLS in realm-export.json -- replace Frontend redirect URLS in realm-export.json
-- SET NODE_ENV=production when running on server -- SET NODE_ENV=production when running on server
-- SET DEFAULT_PERSISSIONS for webapi user -- SET DEFAULT_PERSISSIONS for webapi user
-- add backend to allowed redirect urls in forntenc kc-client
-- fix web origins for kc-clients
+14 -18
View File
@@ -3,38 +3,34 @@
"version": "1.0.0", "version": "1.0.0",
"scripts": { "scripts": {
"start": "node dist/index.js", "start": "node dist/index.js",
"build": "tsc -p .", "build": "tsc -p ."
"dev": "run-script-os",
"dev:win32": "wsl -- bash -ic 'pnpm run dev:linux'",
"dev:linux": "./scripts/dev.sh"
}, },
"type": "module", "type": "module",
"packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a", "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a",
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@types/connect-pg-simple": "^7.0.3",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
"@types/express": "^5.0.6", "@types/connect-pg-simple": "^7.0.3",
"@types/express": "^5.0.5",
"@types/express-session": "^1.18.2", "@types/express-session": "^1.18.2",
"@types/node": "^24.10.1", "@types/node": "^24.9.2",
"@types/pg": "^8.15.6", "@types/pg": "^8.15.6",
"dotenv": "^17.2.3", "prisma": "^7.0.1",
"prisma": "^7.1.0", "tsx": "^4.20.6",
"run-script-os": "^1.1.6",
"tsx": "^4.21.0",
"typescript": "~5.9.3" "typescript": "~5.9.3"
}, },
"dependencies": { "dependencies": {
"@keycloak/keycloak-admin-client": "^26.4.7", "cors": "^2.8.5",
"@prisma/adapter-pg": "^7.1.0", "rxjs": "~7.8.2",
"@prisma/client": "^7.1.0", "@keycloak/keycloak-admin-client": "^26.4.4",
"@prisma/adapter-pg": "^7.0.1",
"@prisma/client": "^7.0.1",
"@tsoa/runtime": "^6.6.0", "@tsoa/runtime": "^6.6.0",
"connect-pg-simple": "^10.0.0", "connect-pg-simple": "^10.0.0",
"cors": "^2.8.5", "dotenv": "^17.2.3",
"express": "^5.2.1", "express": "^5.1.0",
"express-session": "^1.18.2", "express-session": "^1.18.2",
"keycloak-connect": "^26.1.1", "keycloak-connect": "^26.1.1",
"pg": "^8.16.3", "pg": "^8.16.3"
"rxjs": "~7.8.2"
} }
} }
@@ -13,10 +13,10 @@ import {
SuccessResponse, SuccessResponse,
} from "@tsoa/runtime"; } from "@tsoa/runtime";
import express from "express"; import express from "express";
import { Claim } from "../dtos/claim.js";
import type { Member, MemberCreateArgs } from "../dtos/member.js";
import { inject } from "../infrastructure/di/index.js"; import { inject } from "../infrastructure/di/index.js";
import { ErrorStates } from "../infrastructure/errorStates.js"; import { ErrorStates } from "../infrastructure/errorStates.js";
import { Claim } from "../models/claim.js";
import type { Member, MemberCreateArgs } from "../models/member.js";
import { KC_SECURITY_NAME } from "../services/keycloak/user/index.js"; import { KC_SECURITY_NAME } from "../services/keycloak/user/index.js";
import { MembersService } from "../services/membersService.js"; import { MembersService } from "../services/membersService.js";
@@ -9,9 +9,9 @@ import {
Security, Security,
SuccessResponse, SuccessResponse,
} from "@tsoa/runtime"; } from "@tsoa/runtime";
import { Claim } from "../dtos/claim.js";
import { type User } from "../dtos/user.js";
import { inject } from "../infrastructure/di/index.js"; import { inject } from "../infrastructure/di/index.js";
import { Claim } from "../models/claim.js";
import { type User } from "../models/user.js";
import { KC_SECURITY_NAME } from "../services/keycloak/user/index.js"; import { KC_SECURITY_NAME } from "../services/keycloak/user/index.js";
import { UserService } from "../services/userService.js"; import { UserService } from "../services/userService.js";
@@ -1,20 +1,20 @@
import { type MemberModel as DbMember } from "../generated/prisma/models.js"; import { type MemberModel as DbMember } from "../generated/prisma/models.js";
export interface Member { export interface Member {
id?: string | null; id: string | null;
vorname?: string | null; vorname: string | null;
nachname?: string | null; nachname: string | null;
geburtsdatum?: Date | null; geburtsdatum: Date | null;
geburtsort?: string | null; geburtsort: string | null;
geschlecht?: string | null; geschlecht: string | null;
passnummer?: string | null; passnummer: string | null;
uvertrag?: boolean | null; uvertrag: boolean | null;
verein?: boolean | null; verein: boolean | null;
graduierung?: string | null; graduierung: string | null;
letztePruefung?: Date | null; letztePruefung: Date | null;
kontakt?: string | null; kontakt: string | null;
kuendigungzum?: Date | null; kuendigungzum: Date | null;
loeschenam?: string | null; loeschenam: Date | null;
} }
export type MemberCreateArgs = Omit<Member, "id" | "loeschenam">; export type MemberCreateArgs = Omit<Member, "id" | "loeschenam">;
@@ -34,5 +34,6 @@ export function mapDbMemberToMember(dbMember: DbMember): Member {
passnummer: dbMember.passnummer, passnummer: dbMember.passnummer,
uvertrag: dbMember.uvertrag, uvertrag: dbMember.uvertrag,
verein: dbMember.verein, verein: dbMember.verein,
loeschenam: new Date(), //TODO
}; };
} }
+2 -1
View File
@@ -14,7 +14,6 @@ const app = express();
app.use(sessionHandler); app.use(sessionHandler);
app.use(keycloak.middleware()); app.use(keycloak.middleware());
// console.log("not using", keycloak.middleware);
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use(express.json()); app.use(express.json());
@@ -25,6 +24,8 @@ app.use(
"https://tcc-1-ev-intern.web.app", "https://tcc-1-ev-intern.web.app",
"http://localhost:4200", "http://localhost:4200",
"https://localhost:4200", "https://localhost:4200",
"http://localhost:3000",
"http://localhost:8080",
], ],
//TODO reevaluate //TODO reevaluate
}), }),
+24 -7
View File
@@ -8,8 +8,11 @@ function promisifyMiddleware(
response?: Response, response?: Response,
): Promise<any> { ): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
middleware(request, response || ({} as any), (result: any) => { middleware(request, response || ({} as any), (next_result: any) => {
result instanceof Error ? reject(result) : resolve(result); console.dir(next_result);
next_result instanceof Error
? reject(next_result)
: resolve(next_result);
}); });
}); });
} }
@@ -20,10 +23,15 @@ async function doKeycloakAuth(
response?: Response, response?: Response,
): Promise<any> { ): Promise<any> {
if (roles && roles.length > 0) { if (roles && roles.length > 0) {
return await promisifyMiddleware( const grant = await keycloak.getGrant(request, response ?? ({} as any));
request, const allowed = roles.every(role =>
keycloak.enforcer(roles?.map(role => `realm:${role}`)), grant.access_token?.hasRealmRole(role),
); );
if (allowed) {
return Promise.resolve();
} else {
return Promise.reject(httpError(403, "Forbidden"));
}
} else { } else {
return await promisifyMiddleware(request, keycloak.protect(), response); return await promisifyMiddleware(request, keycloak.protect(), response);
} }
@@ -35,12 +43,21 @@ export async function expressAuthentication(
roles?: string[], roles?: string[],
response?: Response, response?: Response,
): Promise<any> { ): Promise<any> {
if (request.method === "OPTIONS") {
return Promise.resolve();
}
switch (securityName) { switch (securityName) {
case KC_SECURITY_NAME: case KC_SECURITY_NAME:
return doKeycloakAuth(request, roles, response); return await doKeycloakAuth(request, roles, response);
case undefined: case undefined:
return Promise.resolve(); return Promise.resolve();
default: default:
return Promise.reject("Unknown security name"); return Promise.reject(httpError(400, "Unknown security name"));
} }
} }
function httpError(status: number, message: string) {
const err: any = new Error(message);
err.status = status;
return err;
}
+6 -11
View File
@@ -4,10 +4,10 @@ import { PrismaClient } from "../../generated/prisma/client.js";
import { Injectable } from "../../infrastructure/di/injectable-decorator.js"; import { Injectable } from "../../infrastructure/di/injectable-decorator.js";
type ExtendedClient = ReturnType<typeof createExtendedClient>; type ExtendedClient = ReturnType<typeof createExtendedClient>;
type TransactionClient = Omit< // type TransactionClient = Omit<
ExtendedClient, // ExtendedClient,
"$connect" | "$disconnect" | "$on" | "$transaction" | "$extends" // "$connect" | "$disconnect" | "$on" | "$transaction" | "$extends"
>; // >;
export type SafeClient = Omit< export type SafeClient = Omit<
ExtendedClient, ExtendedClient,
@@ -58,13 +58,8 @@ export class DatabaseService {
command: (prisma: SafeClient) => Promise<T>, command: (prisma: SafeClient) => Promise<T>,
request: Request, request: Request,
): Promise<T> { ): Promise<T> {
const transaction = (request as any).transaction as SafeClient; const transaction =
(request as any).transaction ?? (this.prismaClient as SafeClient);
return await command(transaction); return await command(transaction);
} }
public async createTransaction(
action: (prisma: TransactionClient) => Promise<void>,
) {
await this.prismaClient.$transaction(action);
}
} }
@@ -3,8 +3,8 @@ import kcClientConfig from "./nodejs-api.json" with { type: "json" };
import KeycloakAdminClient from "@keycloak/keycloak-admin-client"; import KeycloakAdminClient from "@keycloak/keycloak-admin-client";
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation.js"; import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation.js";
import { Claim } from "../../../models/claim.js"; import { Claim } from "../../../dtos/claim.js";
import { type User } from "../../../models/user.js"; import { type User } from "../../../dtos/user.js";
@Injectable() @Injectable()
export class KeycloakAdmin { export class KeycloakAdmin {
@@ -1,10 +1,8 @@
{ {
"realm": "taekwondo", "realm": "taekwondo",
"auth-server-url": "https://localhost:8080", "auth-server-url": "http://localhost:8080/",
"ssl-required": "external", "ssl-required": "external",
"resource": "nodejs-api", "resource": "angular-frontend",
"credentials": { "public-client": true,
"secret": "devsecret"
},
"confidential-port": 0 "confidential-port": 0
} }
+7 -3
View File
@@ -1,11 +1,11 @@
import express from "express"; import express from "express";
import { inject, Injectable } from "../infrastructure/di/index.js";
import { ErrorStates } from "../infrastructure/errorStates.js";
import { import {
type Member, type Member,
type MemberCreateArgs, type MemberCreateArgs,
mapDbMemberToMember, mapDbMemberToMember,
} from "../models/member.js"; } from "../dtos/member.js";
import { inject, Injectable } from "../infrastructure/di/index.js";
import { ErrorStates } from "../infrastructure/errorStates.js";
import { DatabaseService } from "./db/prisma.js"; import { DatabaseService } from "./db/prisma.js";
@Injectable() @Injectable()
@@ -43,6 +43,10 @@ export class MembersService {
createArgs: MemberCreateArgs, createArgs: MemberCreateArgs,
request: express.Request, request: express.Request,
): Promise<Member> { ): Promise<Member> {
const x: MemberCreateArgs = {
...createArgs,
geburtsdatum: createArgs.geburtsdatum ?? null,
};
const newMember = await this.database.doRequest( const newMember = await this.database.doRequest(
async prisma => await prisma.member.create({ data: createArgs }), async prisma => await prisma.member.create({ data: createArgs }),
request, request,
+2 -2
View File
@@ -1,6 +1,6 @@
import { Claim } from "../dtos/claim.js";
import { type User } from "../dtos/user.js";
import { inject } from "../infrastructure/di/index.js"; import { inject } from "../infrastructure/di/index.js";
import { Claim } from "../models/claim.js";
import { type User } from "../models/user.js";
import { KeycloakAdmin } from "./keycloak/admin/index.js"; import { KeycloakAdmin } from "./keycloak/admin/index.js";
export class UserService { export class UserService {
+14
View File
@@ -0,0 +1,14 @@
{
"tabWidth": 4,
"arrowParens": "avoid",
"bracketSameLine": true,
"semi": true,
"overrides": [
{
"files": "*.yml",
"options": {
"tabWidth": 2
}
}
]
}
+4
View File
@@ -29,9 +29,13 @@
"@angular/core": "^20.3.15", "@angular/core": "^20.3.15",
"@angular/forms": "^20.3.15", "@angular/forms": "^20.3.15",
"@angular/material": "20.2.11", "@angular/material": "20.2.11",
"@angular/material-date-fns-adapter": "^21.0.2",
"@angular/platform-browser": "^20.3.15", "@angular/platform-browser": "^20.3.15",
"@angular/router": "^20.3.15", "@angular/router": "^20.3.15",
"@date-fns/tz": "^1.4.1",
"@date-fns/utc": "^2.1.1",
"@oazapfts/runtime": "^1.0.4", "@oazapfts/runtime": "^1.0.4",
"date-fns": "^4.1.0",
"keycloak-angular": "^20.0.0", "keycloak-angular": "^20.0.0",
"keycloak-js": "^26.2.1", "keycloak-js": "^26.2.1",
"rxjs": "~7.8.2", "rxjs": "~7.8.2",
+37 -5
View File
@@ -1,6 +1,6 @@
import { import {
ApplicationConfig, ApplicationConfig,
isDevMode, LOCALE_ID,
provideBrowserGlobalErrorListeners, provideBrowserGlobalErrorListeners,
provideZoneChangeDetection, provideZoneChangeDetection,
} from '@angular/core'; } from '@angular/core';
@@ -21,22 +21,40 @@ import {
withAutoRefreshToken, withAutoRefreshToken,
} from 'keycloak-angular'; } from 'keycloak-angular';
import {
DateFnsAdapter,
MAT_DATE_FNS_FORMATS,
} from '@angular/material-date-fns-adapter';
import {
DateAdapter,
MAT_DATE_FORMATS,
MAT_DATE_LOCALE,
} from '@angular/material/core';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { de } from 'date-fns/locale';
import { routes } from './app.routes'; import { routes } from './app.routes';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideRouter(routes, withPreloading(PreloadAllModules), withComponentInputBinding()), provideRouter(
routes,
withPreloading(PreloadAllModules),
withComponentInputBinding(),
),
provideBrowserGlobalErrorListeners(), provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }), provideZoneChangeDetection({ eventCoalescing: true }),
provideKeycloak({ provideKeycloak({
config: { config: {
url: isDevMode() ? 'http://localhost:8080/' : 'https://keycloak.toni714.de:8443/', url: true
? 'http://localhost:8080/'
: 'https://keycloak.toni714.de:8443/',
realm: 'taekwondo', realm: 'taekwondo',
clientId: 'angular-frontend', clientId: 'angular-frontend',
}, },
initOptions: { initOptions: {
onLoad: 'check-sso', onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html', silentCheckSsoRedirectUri:
window.location.origin + '/silent-check-sso.html',
}, },
features: [withAutoRefreshToken({ sessionTimeout: 600000 })], features: [withAutoRefreshToken({ sessionTimeout: 600000 })],
providers: [AutoRefreshTokenService, UserActivityService], providers: [AutoRefreshTokenService, UserActivityService],
@@ -45,9 +63,23 @@ export const appConfig: ApplicationConfig = {
provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG, provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
useValue: [ useValue: [
createInterceptorCondition<IncludeBearerTokenCondition>({ createInterceptorCondition<IncludeBearerTokenCondition>({
urlPattern: isDevMode()? /^http:\/\/localhost:3000.*$/ : /^https:\/\/tkd-api\.toni714\.de.*$/, urlPattern: true
? /^http:\/\/localhost:(3000|8080)\/.*$/
: /^https:\/\/tkd-api\.toni714\.de\/.*$/,
}), }),
], ],
}, },
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: { appearance: 'outline', subscriptSizing: 'dynamic' },
},
{ provide: LOCALE_ID, useValue: 'de' },
{
provide: DateAdapter,
useClass: DateFnsAdapter,
deps: [MAT_DATE_LOCALE],
},
{ provide: MAT_DATE_FORMATS, useValue: MAT_DATE_FNS_FORMATS },
{ provide: MAT_DATE_LOCALE, useValue: de },
], ],
}; };
+25 -6
View File
@@ -10,24 +10,28 @@ import {
import { AuthGuardData, createAuthGuard } from 'keycloak-angular'; import { AuthGuardData, createAuthGuard } from 'keycloak-angular';
import { Login } from './components/login/login'; import { Login } from './components/login/login';
import { MissingPage } from './components/missing-page/missing-page'; import { MissingPage } from './components/missing-page/missing-page';
import { Claim } from './generated-api/api';
export const roles = { export const roles = {
UserAdmin: 'useradmin', UserAdmin: 'useradmin',
MemberAdmin: 'memberadmin', MemberAdmin: 'memberadmin',
}; };
function requireRoles(roles: string[]=[], any: boolean = false): CanActivateFn { function requireRoles(
roles: string[] = [],
any: boolean = false,
): CanActivateFn {
const isAllowed = async ( const isAllowed = async (
_: ActivatedRouteSnapshot, _: ActivatedRouteSnapshot,
__: RouterStateSnapshot, __: RouterStateSnapshot,
authData: AuthGuardData authData: AuthGuardData,
): Promise<boolean | UrlTree> => { ): Promise<boolean | UrlTree> => {
const { authenticated, grantedRoles } = authData; const { authenticated, grantedRoles } = authData;
const allRoles = Object.values(grantedRoles).flat(); const allRoles = Object.values(grantedRoles).flat();
const hasRequiredRoles = any const hasRequiredRoles = any
? roles.some((role) => allRoles.includes(role)) ? roles.some(role => allRoles.includes(role))
: roles.every((role) => allRoles.includes(role)); : roles.every(role => allRoles.includes(role));
if (authenticated && hasRequiredRoles) { if (authenticated && hasRequiredRoles) {
return true; return true;
@@ -43,13 +47,28 @@ export const routes: Routes = [
{ path: 'login', component: Login }, { path: 'login', component: Login },
{ {
path: '', path: '',
loadComponent: () => import('./components/home/home').then((mod) => mod.Home), loadComponent: () =>
import('./components/home/home').then(mod => mod.Home),
canActivate: [requireRoles()], canActivate: [requireRoles()],
}, },
{ {
path: 'home', path: 'home',
loadComponent: () => import('./components/home/home').then((mod) => mod.Home), loadComponent: () =>
import('./components/home/home').then(mod => mod.Home),
canActivate: [requireRoles()], canActivate: [requireRoles()],
}, },
{
path: 'test',
loadComponent: () =>
import('./components/test/test').then(mod => mod.Test),
},
{
path: 'list',
loadComponent: () =>
import('./components/view-members/view-members').then(
mod => mod.ViewMembers,
),
canActivate: [requireRoles([Claim.Memberadmin])],
},
{ path: '**', component: MissingPage }, { path: '**', component: MissingPage },
]; ];
@@ -0,0 +1,13 @@
<form [formGroup]="form" (ngSubmit)="submit()">
<p>Name: {{ member.vorname }} {{ member.nachname }}</p>
<p>Passnummer: {{ member.passnummer }}</p>
<p>Geburtsdatum: {{ member.geburtsdatum??undefined | datePipe }}</p>
<mat-form-field>
<mat-label>K&uuml;ndigun zum</mat-label>
<input matInput [matDatepicker]="picker" formControlName="kuendigungzum" />
<mat-hint>DD.MM.YYYY</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<button mat-raised-button color="primary" type="submit">Speichern</button>
</form>
@@ -0,0 +1,11 @@
form{
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
height: 100%;
}
form > *{
margin-top: 20px;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DeleteMemberDialogComponent } from './delete-member-dialog.component.js';
describe('DeleteMemberDialogComponent', () => {
let component: DeleteMemberDialogComponent;
let fixture: ComponentFixture<DeleteMemberDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DeleteMemberDialogComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DeleteMemberDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,57 @@
import { Component, Inject } from '@angular/core';
import {
FormControl,
FormGroup,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatOptionModule } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
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 { EditMemberDialogComponent } from '../edit-member-dialog/edit-member-dialog.component.js';
@Component({
selector: 'app-delete-member-dialog',
standalone: true,
imports: [
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatCheckboxModule,
MatButtonModule,
MatSelectModule,
MatOptionModule,
MatDatepickerModule,
DatePipePipe,
],
templateUrl: './delete-member-dialog.component.html',
styleUrl: './delete-member-dialog.component.scss',
})
export class DeleteMemberDialogComponent {
form: FormGroup;
member: api.Member;
constructor(
@Inject(MAT_DIALOG_DATA) public data: { member: api.Member },
private dialogRef: MatDialogRef<EditMemberDialogComponent>,
) {
this.member = data.member;
this.form = new FormGroup({
kuendigungzum: new FormControl('', [Validators.required]),
});
}
async submit() {
await api.scheduleDeletion(
this.member.id!,
this.form.controls['kuendigungzum'].value,
);
this.dialogRef.close();
}
}
@@ -0,0 +1,44 @@
<form [formGroup]="form" (ngSubmit)="submit()">
<mat-form-field>
<mat-label>Vorname</mat-label>
<input matInput placeholder="Vorname" formControlName="vorname" />
</mat-form-field>
<mat-form-field>
<mat-label>Nachname</mat-label>
<input matInput placeholder="Nachname" formControlName="nachname" />
</mat-form-field>
<mat-form-field>
<mat-label>Geschlecht</mat-label>
<input matInput placeholder="Geschlecht" formControlName="geschlecht" />
</mat-form-field>
<mat-form-field>
<mat-label>Passnummer</mat-label>
<input matInput placeholder="Passnummer" formControlName="passnummer" />
</mat-form-field>
<mat-form-field>
<mat-label>Geburtsdatum</mat-label>
<input matInput [matDatepicker]="picker" formControlName="geburtsdatum" />
<mat-hint>DD.MM.YYYY</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<mat-form-field>
<mat-label>Geburtsort</mat-label>
<input matInput placeholder="Geburtsort" formControlName="geburtsort" />
</mat-form-field>
<mat-form-field>
<mat-label>Unterrichtsvertrag?</mat-label>
<mat-select matInput formControlName="uvertrag">
<mat-option [value]="true">Ja</mat-option>
<mat-option [value]="false">Nein</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label>Vereinsmitglied?</mat-label>
<mat-select matInput formControlName="verein">
<mat-option [value]="true">Ja</mat-option>
<mat-option [value]="false">Nein</mat-option>
</mat-select>
</mat-form-field>
<button mat-raised-button color="primary" type="submit">Speichern</button>
</form>
@@ -0,0 +1,8 @@
form{
display: flex;
flex-direction: column;
}
form > *{
margin-top: 20px;
}
@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EditMemberDialogComponent } from './edit-member-dialog.component.js';
describe('EditMemberDialogComponent', () => {
let component: EditMemberDialogComponent;
let fixture: ComponentFixture<EditMemberDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [EditMemberDialogComponent],
}).compileComponents();
fixture = TestBed.createComponent(EditMemberDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,47 @@
import { Component, Inject } from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatOptionModule } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
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 { MemberDtoHelper } from '../../util/member.js';
@Component({
selector: 'app-edit-member-dialog',
standalone: true,
imports: [
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatCheckboxModule,
MatButtonModule,
MatSelectModule,
MatOptionModule,
MatDatepickerModule,
],
templateUrl: './edit-member-dialog.component.html',
styleUrl: './edit-member-dialog.component.scss',
})
export class EditMemberDialogComponent {
form: FormGroup;
constructor(
@Inject(MAT_DIALOG_DATA) public data: { member: api.Member },
private memberDtoHelper: MemberDtoHelper,
private dialogRef: MatDialogRef<EditMemberDialogComponent>,
) {
this.form = this.memberDtoHelper.newMemberForm(data.member);
}
submit() {
const member: api.Member = this.memberDtoHelper.memberFromForm(
this.form,
);
api.updateMember(member);
this.dialogRef.close();
}
}
@@ -1 +1,6 @@
<p>missing-page works!</p> <p>Diese Seite existiert nicht!</p>
@if(this.auth.loggedIn()){
<a mat-stroked-button [routerLink]="['/home']">Zur Starteite</a>
}@else {
<a mat-stroked-button [routerLink]="['/']">Zur Login Seite</a>
}
@@ -1,11 +1,14 @@
import { Component } from '@angular/core'; import { Component, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { Authentication } from '../../services/authentication';
@Component({ @Component({
selector: 'app-missing-page', selector: 'app-missing-page',
imports: [], imports: [MatButtonModule, RouterModule],
templateUrl: './missing-page.html', templateUrl: './missing-page.html',
styleUrl: './missing-page.scss', styleUrl: './missing-page.scss',
}) })
export class MissingPage { export class MissingPage {
protected readonly auth = inject(Authentication);
} }
@@ -0,0 +1,39 @@
<h1>Neues Mitglied anlegen</h1>
<form [formGroup]="memberForm" (ngSubmit)="onSubmit()">
<mat-form-field>
<mat-label>Vorname</mat-label>
<input matInput formControlName="vorname" />
</mat-form-field>
<mat-form-field>
<mat-label>Nachname</mat-label>
<input matInput formControlName="nachname" />
</mat-form-field>
<mat-form-field>
<mat-label>Geschlecht</mat-label>
<input matInput formControlName="geschlecht" />
</mat-form-field>
<mat-form-field>
<mat-label>Geburtsdatum</mat-label>
<input matInput [matDatepicker]="picker" formControlName="geburtsdatum" />
<mat-hint>DD.MM.YYYY</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<mat-form-field>
<mat-label>Geburtsort</mat-label>
<input matInput formControlName="geburtsort" />
</mat-form-field>
<mat-checkbox formControlName="uvertrag"
>Unterrichtsvertrag Abgegeben?</mat-checkbox
>
<mat-checkbox formControlName="uvertrag">Verein Beigetreten?</mat-checkbox>
<mat-form-field>
<mat-label>Passnummer (falls vorhanden)</mat-label>
<input matInput formControlName="passnummer" />
</mat-form-field>
<mat-form-field>
<mat-label>Kontakt</mat-label>
<input matInput formControlName="kontakt" />
</mat-form-field>
<button mat-flat-button color="primary" type="submit">Hinzufügen</button>
</form>
@@ -0,0 +1,9 @@
form {
display: flex;
flex-direction: column;
align-items: center;
}
h1 {
text-align: center;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NewMemberComponent } from './new-member.component.js';
describe('NewMemberComponent', () => {
let component: NewMemberComponent;
let fixture: ComponentFixture<NewMemberComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NewMemberComponent]
})
.compileComponents();
fixture = TestBed.createComponent(NewMemberComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,49 @@
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import * as api from '../../generated-api/api.js';
import { MemberDtoHelper } from '../../util/member.js';
@Component({
selector: 'app-new-member',
standalone: true,
providers: [],
imports: [
ReactiveFormsModule,
MatFormFieldModule,
MatDatepickerModule,
MatInputModule,
MatCheckboxModule,
MatButtonModule,
],
templateUrl: './new-member.component.html',
styleUrl: './new-member.component.scss',
})
export class NewMemberComponent {
private readonly memberDtoHelper = inject(MemberDtoHelper);
constructor() {}
memberForm = this.memberDtoHelper.newMemberForm();
private dialogRef: MatDialogRef<NewMemberComponent> = inject(
MatDialogRef<NewMemberComponent>,
);
onSubmit() {
const newMember: api.PickMemberExcludeKeyofMemberIdOrLoeschenam =
this.memberDtoHelper.newMemberFromForm(this.memberForm);
api.createMember(newMember)
.then(() => {
console.log('Successfully added Member');
this.dialogRef.close();
})
.catch(e => {
console.error('Error adding member', e);
});
}
}
@@ -1 +1,17 @@
<p>test works!</p> <h1>Testeite für interne Berechtigung</h1>
<p>API: {{apiStatus()}}</p>
<p>Auth: {{authTest()}}</p>
Claim:
<ul>
@let claims = this.auth.claims();
@if(claims) {
<pre>
{{claims | json}}
</pre>
}
</ul>
<details name="UserInfo:">
<pre>
{{this.auth.userInfo() | json}}
</pre>
</details>
@@ -1,11 +1,42 @@
import { Component } from '@angular/core'; import { JsonPipe } from '@angular/common';
import { Component, effect, inject, signal } from '@angular/core';
import * as api from '../../generated-api/api';
import { Authentication } from '../../services/authentication';
@Component({ @Component({
selector: 'app-test', selector: 'app-test',
imports: [], imports: [JsonPipe],
templateUrl: './test.html', templateUrl: './test.html',
styleUrl: './test.scss', styleUrl: './test.scss',
}) })
export class Test { export class Test {
protected auth = inject(Authentication);
protected apiStatus = signal<string>("Waiting");
protected authTest = signal<string>("Waiting");
public constructor(){
effect(()=>{
const loggedIn=this.auth.loggedIn();
if(!loggedIn){
this.apiStatus.set("N/A - not logged in");
this.authTest.set("N/A - not logged in");
return;
}
this.apiStatus.set("Waiting for API");
this.authTest.set("Waiting for API");
api.getOk().then((status) => {
this.apiStatus.set("Ok");
}).catch((e) => {
this.apiStatus.set("Error");
});
api.getAuth().then((response) => {
this.authTest.set("Ok");
}).catch((e) => {
this.authTest.set(e);
});
})
}
} }
@@ -0,0 +1,125 @@
<h1>Mitglieder & Sportlerübersicht</h1>
<button mat-raised-button color="accent" (click)="newMember()">
Neues Mitglied
</button>
<div class="filter-container mat-elevation-z8">
<div class="filter-header">
<mat-form-field>
<input
matInput
(keyup)="applyFilter($event)"
[value]="filter$ | async"
placeholder="Filter"
/>
</mat-form-field>
</div>
</div>
<ng-container *ngIf="dataSource$ | async as dataSource">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container matColumnDef="edit">
<th mat-header-cell *matHeaderCellDef>Bearb.</th>
<td mat-cell *matCellDef="let element">
<button
mat-mini-fab-button
(click)="editMember(element.id)"
aria-label="Edit Member"
>
<mat-icon fontIcon="edit"></mat-icon>
</button>
</td>
</ng-container>
<ng-container matColumnDef="delete">
<th mat-header-cell *matHeaderCellDef>Entf.</th>
<td mat-cell *matCellDef="let element">
<button
mat-mini-fab-button
(click)="deleteMember(element.id)"
aria-label="Delete Member"
>
<mat-icon fontIcon="delete"></mat-icon>
</button>
</td>
</ng-container>
<ng-container matColumnDef="vorname">
<th mat-header-cell *matHeaderCellDef>Vorname</th>
<td mat-cell *matCellDef="let element">
<span [innerHTML]="element.vorname | stylizeNames"></span>
</td>
</ng-container>
<ng-container matColumnDef="nachname">
<th mat-header-cell *matHeaderCellDef>Nachname</th>
<td mat-cell *matCellDef="let element">
<span [innerHTML]="element.nachname | stylizeNames"></span>
</td>
</ng-container>
<ng-container matColumnDef="geschlecht">
<th mat-header-cell *matHeaderCellDef>Geschlecht</th>
<td mat-cell *matCellDef="let element">{{ element.geschlecht }}</td>
</ng-container>
<ng-container matColumnDef="passnummer">
<th mat-header-cell *matHeaderCellDef>Passnummer</th>
<td mat-cell *matCellDef="let element">
{{ element.passnummer | nobreakHyphen }}
</td>
</ng-container>
<ng-container matColumnDef="geburtsdatum">
<th mat-header-cell *matHeaderCellDef>Geburtsdatum</th>
<td mat-cell *matCellDef="let element">
{{ element.geburtsdatum | datePipe }}
</td>
</ng-container>
<ng-container matColumnDef="geburtsort">
<th mat-header-cell *matHeaderCellDef>Geburtsort</th>
<td mat-cell *matCellDef="let element">{{ element.geburtsort }}</td>
</ng-container>
<ng-container matColumnDef="uvertrag">
<th mat-header-cell *matHeaderCellDef>Unterrichtsvertrag</th>
<td mat-cell *matCellDef="let element">
<mat-icon
*ngIf="element.uvertrag"
class="positive"
aria-label="Unterrichtsvertrag Check"
fontIcon="check"
></mat-icon>
<mat-icon
*ngIf="!element.uvertrag"
class="negative"
aria-label="Unterrichtsvertrag Cross"
fontIcon="close"
></mat-icon>
</td>
</ng-container>
<ng-container matColumnDef="verein">
<th mat-header-cell *matHeaderCellDef>Vereinsmitgliedschaft</th>
<td mat-cell *matCellDef="let element">
<mat-icon
*ngIf="element.verein"
class="positive"
aria-label="Verein Check"
fontIcon="check"
></mat-icon>
<mat-icon
*ngIf="!element.verein"
class="negative"
aria-label="Verein Cross"
fontIcon="close"
></mat-icon>
</td>
</ng-container>
<ng-container matColumnDef="kontakt">
<th mat-header-cell *matHeaderCellDef>Kontakt</th>
<td mat-cell *matCellDef="let element">{{ element.kontakt }}</td>
</ng-container>
<ng-container matColumnDef="kuendigungzum">
<th mat-header-cell *matHeaderCellDef>K&uuml;ndigung zum</th>
<td mat-cell *matCellDef="let element">
{{ (element.kuendigungzum | datePipe) ?? "-" }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</ng-container>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ViewMembers } from './view-members';
describe('ViewMembers', () => {
let component: ViewMembers;
let fixture: ComponentFixture<ViewMembers>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ViewMembers]
})
.compileComponents();
fixture = TestBed.createComponent(ViewMembers);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,115 @@
import { AsyncPipe, NgIf } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
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 { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { RouterModule } from '@angular/router';
import { BehaviorSubject, firstValueFrom, from, Observable } 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';
import { StylizeNamesPipe } from '../../pipes/stylize-names.pipe.js';
import { DeleteMemberDialogComponent } from '../delete-member-dialog/delete-member-dialog.component.js';
import { EditMemberDialogComponent } from '../edit-member-dialog/edit-member-dialog.component.js';
import { NewMemberComponent } from '../new-member/new-member.component';
@Component({
selector: 'app-view-members',
imports: [
MatTableModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
NgIf,
StylizeNamesPipe,
NobreakHyphenPipe,
RouterModule,
MatButtonModule,
AsyncPipe,
DatePipePipe,
],
templateUrl: './view-members.html',
styleUrl: './view-members.scss',
})
export class ViewMembers {
async deleteMember(memberId: any) {
const dataSource = await firstValueFrom(this.dataSource$);
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<MatTableDataSource<api.Member>> =
new BehaviorSubject(new MatTableDataSource([] as api.Member[]));
filter$: BehaviorSubject<string> = new BehaviorSubject('');
displayedColumns: string[] = [
'edit',
'delete',
'vorname',
'nachname',
'geschlecht',
'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);
}
constructor(private dialog: MatDialog) {
this.dataSource$ = this.updateData();
}
updateData() {
return from(
api.getMembers().then(members => {
let dataSource = new MatTableDataSource<api.Member>(members);
this.filter$.subscribe(filter => {
dataSource.filter = filter;
});
return dataSource;
}),
);
}
}
@@ -0,0 +1,8 @@
import { DatePipePipe } from './date-pipe.pipe';
describe('DatePipePipe', () => {
it('create an instance', () => {
const pipe = new DatePipePipe();
expect(pipe).toBeTruthy();
});
});
@@ -0,0 +1,20 @@
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
})
export class DatePipePipe implements PipeTransform {
constructor(@Inject(MAT_DATE_LOCALE) private locale: 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 });
}
}
@@ -0,0 +1,8 @@
import { NobreakHyphenPipe } from './nobreak-hyphen.pipe.js';
describe('NobreakHyphenPipe', () => {
it('create an instance', () => {
const pipe = new NobreakHyphenPipe();
expect(pipe).toBeTruthy();
});
});
@@ -0,0 +1,11 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'nobreakHyphen',
standalone: true,
})
export class NobreakHyphenPipe implements PipeTransform {
transform(value: string): unknown {
return value?.replaceAll('-', ''); // second '-' is a &#8209; "no-break hyphen", first is a regular hyphen
}
}
@@ -0,0 +1,8 @@
import { StylizeNamesPipe } from './stylize-names.pipe.js';
describe('StylizeNamesPipe', () => {
it('create an instance', () => {
const pipe = new StylizeNamesPipe();
expect(pipe).toBeTruthy();
});
});
@@ -0,0 +1,41 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'stylizeNames',
standalone: true,
})
export class StylizeNamesPipe implements PipeTransform {
transform(input?: string): string|undefined {
if (!input) {
return input;
}
let formatted = '';
formatted=this.capitalizeFirstLetters(input);
formatted=this.boldUnderscored(formatted);
return formatted;
}
boldUnderscored(input: string): string {
const segments=input.split('_');
for(let i=0;i<Math.floor(segments.length/2);i++){
if(segments.length>=2*i+1){
segments[2*i+1]='<b>'+segments[2*i+1]+'</b>';
}
}
return segments.join('');
}
capitalizeFirstLetters(input: string): string {
const words=input.split(' ');
for(let i=0;i<words.length;i++){
if(words[i].charAt(0)=='_'){
words[i]=words[i].charAt(0)+words[i].charAt(1).toUpperCase()+words[i].slice(2);
}else{
words[i]=words[i].charAt(0).toUpperCase()+words[i].slice(1);
}
}
return words.join(' ');
}
}
@@ -6,6 +6,7 @@ import {
typeEventArgs, typeEventArgs,
} from 'keycloak-angular'; } from 'keycloak-angular';
import Keycloak, { KeycloakProfile } from 'keycloak-js'; import Keycloak, { KeycloakProfile } from 'keycloak-js';
import { defaults as apiDefaults, Claim } from '../generated-api/api';
export enum AuthenticationState { export enum AuthenticationState {
Authenticated, Authenticated,
@@ -19,42 +20,64 @@ export enum AuthenticationState {
export class Authentication { export class Authentication {
private readonly keycloak = inject(Keycloak); private readonly keycloak = inject(Keycloak);
private readonly keycloakSignal = inject(KEYCLOAK_EVENT_SIGNAL); private readonly keycloakSignal = inject(KEYCLOAK_EVENT_SIGNAL);
private readonly _authenticationState = signal<AuthenticationState>(AuthenticationState.Unknown); private readonly _authenticationState = signal<AuthenticationState>(
AuthenticationState.Unknown,
);
private readonly _userInfo = signal<KeycloakProfile | null>(null); private readonly _userInfo = signal<KeycloakProfile | null>(null);
public readonly authenticationState = this._authenticationState.asReadonly(); private readonly _claims = signal<Record<Claim, boolean> | null>(null);
public readonly loggedIn = computed(()=>{ public readonly authenticationState =
this._authenticationState.asReadonly();
public readonly loggedIn = computed(() => {
return this.authenticationState() === AuthenticationState.Authenticated; return this.authenticationState() === AuthenticationState.Authenticated;
}); });
public readonly userInfo = this._userInfo.asReadonly(); public readonly userInfo = this._userInfo.asReadonly();
public readonly claims = this._claims.asReadonly();
public constructor() { public constructor() {
effect(() => { effect(() => {
const event = this.keycloakSignal(); const event = this.keycloakSignal();
apiDefaults.headers['Authorization'] = this.keycloak.token
? `Bearer ${this.keycloak.token}`
: undefined;
switch (event?.type) { switch (event?.type) {
case KeycloakEventType.Ready: case KeycloakEventType.Ready:
this._authenticationState.set( this._authenticationState.set(
typeEventArgs<ReadyArgs>(event.args) typeEventArgs<ReadyArgs>(event.args)
? AuthenticationState.Authenticated ? AuthenticationState.Authenticated
: AuthenticationState.Unauthenticated : AuthenticationState.Unauthenticated,
); );
break; break;
case KeycloakEventType.AuthSuccess: case KeycloakEventType.AuthSuccess:
this._authenticationState.set(AuthenticationState.Authenticated); this._authenticationState.set(
AuthenticationState.Authenticated,
);
break; break;
case KeycloakEventType.AuthLogout: case KeycloakEventType.AuthLogout:
this._authenticationState.set(AuthenticationState.Unauthenticated); this._authenticationState.set(
AuthenticationState.Unauthenticated,
);
break; break;
case KeycloakEventType.AuthError: case KeycloakEventType.AuthError:
this._authenticationState.set(AuthenticationState.Unauthenticated); this._authenticationState.set(
AuthenticationState.Unauthenticated,
);
break; break;
case KeycloakEventType.AuthRefreshError: case KeycloakEventType.AuthRefreshError:
this._authenticationState.set(AuthenticationState.Unauthenticated); this._authenticationState.set(
AuthenticationState.Unauthenticated,
);
break; break;
case KeycloakEventType.AuthRefreshSuccess: case KeycloakEventType.AuthRefreshSuccess:
this._authenticationState.set(AuthenticationState.Authenticated); this._authenticationState.set(
AuthenticationState.Authenticated,
);
break; break;
case KeycloakEventType.TokenExpired: case KeycloakEventType.TokenExpired:
this._authenticationState.set(AuthenticationState.Unauthenticated); this._authenticationState.set(
AuthenticationState.Unauthenticated,
);
break; break;
default: default:
break; break;
@@ -67,12 +90,23 @@ export class Authentication {
this._userInfo.set(null); this._userInfo.set(null);
} }
const profile = await this.keycloak.loadUserProfile(); const profile = await this.keycloak.loadUserProfile();
const claims: Partial<Record<Claim, boolean>> = {};
for (const claim of Object.values(Claim)) {
if (this.keycloak.hasRealmRole(claim)) {
claims[claim] = true;
} else {
claims[claim] = false;
}
}
this._userInfo.set(profile); this._userInfo.set(profile);
this._claims.set(claims as Record<Claim, boolean>);
}); });
} }
public async login(location?: string): Promise<void> { public async login(location?: string): Promise<void> {
if(this._authenticationState() === AuthenticationState.Authenticated){ if (this._authenticationState() === AuthenticationState.Authenticated) {
return; return;
} }
@@ -86,6 +120,8 @@ export class Authentication {
} }
public async logout(): Promise<void> { public async logout(): Promise<void> {
return await this.keycloak.logout({ redirectUri: window.location.origin + '/login' }); return await this.keycloak.logout({
redirectUri: window.location.origin + '/login',
});
} }
} }
+99
View File
@@ -0,0 +1,99 @@
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';
interface MemberForm {
id: FormControl<string | null | undefined>;
vorname: FormControl<string | null | undefined>;
nachname: FormControl<string | null | undefined>;
geschlecht: FormControl<string | null | undefined>;
geburtsdatum: FormControl<Date | null>;
geburtsort: FormControl<string | null | undefined>;
passnummer: FormControl<string | null | undefined>;
uvertrag: FormControl<boolean | null | undefined>;
verein: FormControl<boolean | null | undefined>;
kontakt: FormControl<string | null | undefined>;
graduierung: FormControl<string | null | undefined>;
kuendigungzum: FormControl<string | null | undefined>;
letztePruefung: FormControl<string | null | undefined>;
}
@Injectable({
providedIn: 'root',
})
export class MemberDtoHelper {
constructor(@Inject(MAT_DATE_LOCALE) private locale: Locale) {}
public newMemberForm(member?: Member): FormGroup<MemberForm> {
const geburtsdatum = member?.geburtsdatum;
let enDate: Date | null = null;
if (geburtsdatum) {
enDate = parse(geburtsdatum, 'dd.MM.yyyy', new Date(), {
//TODO get dd.MM.yyyy from MAT_DATE_LOCALE or LOCALE_ID
locale: this.locale,
});
}
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),
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),
});
}
memberFromForm(form: FormGroup<MemberForm>): Member {
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)
: 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
};
}
newMemberFromForm(
form: FormGroup<MemberForm>,
): PickMemberExcludeKeyofMemberIdOrLoeschenam {
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)
: 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
};
}
}
+12 -12
View File
@@ -9,20 +9,20 @@
@include mat.core(); @include mat.core();
@include mat.all-component-themes(my-theme.$light-theme); @include mat.core-theme(my-theme.$primary-palette);
@include mat.color-variants-backwards-compatibility(my-theme.$light-theme); @include mat.button-theme(my-theme.$primary-palette);
html { html {
// @include mat.theme(( @include mat.theme((
// color: ( color: (
// primary: my-theme.$primary-palette, primary: my-theme.$primary-palette,
// tertiary: my-theme.$tertiary-palette, tertiary: my-theme.$tertiary-palette,
// ), ),
// typography: Roboto, typography: Roboto,
// density: 0, density: 0,
// )); ));
@include mat.core-theme(my-theme.$light-theme); @include mat.core-theme(my-theme.$primary-palette);
@include mat.button-theme(my-theme.$light-theme); @include mat.button-theme(my-theme.$primary-palette);
} }
body { body {
+54 -22
View File
@@ -15,13 +15,13 @@ importers:
packages/api: packages/api:
dependencies: dependencies:
'@keycloak/keycloak-admin-client': '@keycloak/keycloak-admin-client':
specifier: ^26.4.7 specifier: ^26.4.4
version: 26.4.7 version: 26.4.7
'@prisma/adapter-pg': '@prisma/adapter-pg':
specifier: ^7.1.0 specifier: ^7.0.1
version: 7.1.0 version: 7.1.0
'@prisma/client': '@prisma/client':
specifier: ^7.1.0 specifier: ^7.0.1
version: 7.1.0(prisma@7.1.0(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(typescript@5.9.3) version: 7.1.0(prisma@7.1.0(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(typescript@5.9.3)
'@tsoa/runtime': '@tsoa/runtime':
specifier: ^6.6.0 specifier: ^6.6.0
@@ -32,8 +32,11 @@ importers:
cors: cors:
specifier: ^2.8.5 specifier: ^2.8.5
version: 2.8.5 version: 2.8.5
dotenv:
specifier: ^17.2.3
version: 17.2.3
express: express:
specifier: ^5.2.1 specifier: ^5.1.0
version: 5.2.1 version: 5.2.1
express-session: express-session:
specifier: ^1.18.2 specifier: ^1.18.2
@@ -55,28 +58,22 @@ importers:
specifier: ^2.8.19 specifier: ^2.8.19
version: 2.8.19 version: 2.8.19
'@types/express': '@types/express':
specifier: ^5.0.6 specifier: ^5.0.5
version: 5.0.6 version: 5.0.6
'@types/express-session': '@types/express-session':
specifier: ^1.18.2 specifier: ^1.18.2
version: 1.18.2 version: 1.18.2
'@types/node': '@types/node':
specifier: ^24.10.1 specifier: ^24.9.2
version: 24.10.1 version: 24.10.1
'@types/pg': '@types/pg':
specifier: ^8.15.6 specifier: ^8.15.6
version: 8.15.6 version: 8.15.6
dotenv:
specifier: ^17.2.3
version: 17.2.3
prisma: prisma:
specifier: ^7.1.0 specifier: ^7.0.1
version: 7.1.0(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) version: 7.1.0(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
run-script-os:
specifier: ^1.1.6
version: 1.1.6
tsx: tsx:
specifier: ^4.21.0 specifier: ^4.20.6
version: 4.21.0 version: 4.21.0
typescript: typescript:
specifier: ~5.9.3 specifier: ~5.9.3
@@ -102,15 +99,27 @@ importers:
'@angular/material': '@angular/material':
specifier: 20.2.11 specifier: 20.2.11
version: 20.2.11(437fd31793958c779574d25f524a2c50) 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)
'@angular/platform-browser': '@angular/platform-browser':
specifier: ^20.3.15 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)) 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))
'@angular/router': '@angular/router':
specifier: ^20.3.15 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))(@angular/platform-browser@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)))(rxjs@7.8.2) 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))(@angular/platform-browser@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)))(rxjs@7.8.2)
'@date-fns/tz':
specifier: ^1.4.1
version: 1.4.1
'@date-fns/utc':
specifier: ^2.1.1
version: 2.1.1
'@oazapfts/runtime': '@oazapfts/runtime':
specifier: ^1.0.4 specifier: ^1.0.4
version: 1.0.4 version: 1.0.4
date-fns:
specifier: ^4.1.0
version: 4.1.0
keycloak-angular: keycloak-angular:
specifier: ^20.0.0 specifier: ^20.0.0
version: 20.0.0(@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))(@angular/router@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))(@angular/platform-browser@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)))(rxjs@7.8.2))(keycloak-js@26.2.1)(rxjs@7.8.2) version: 20.0.0(@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))(@angular/router@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))(@angular/platform-browser@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)))(rxjs@7.8.2))(keycloak-js@26.2.1)(rxjs@7.8.2)
@@ -405,6 +414,13 @@ packages:
'@angular/platform-browser': 20.3.15 '@angular/platform-browser': 20.3.15
rxjs: ^6.5.3 || ^7.4.0 rxjs: ^6.5.3 || ^7.4.0
'@angular/material-date-fns-adapter@21.0.2':
resolution: {integrity: sha512-vNJSiC6Ik6jmtdLUPWm514yJjoRP6blpw0DsACLcEDZJuIujAtvN/YmE3e+azk3Psr3nJgR3p029nQmKfHm7pw==}
peerDependencies:
'@angular/core': ^21.0.0 || ^22.0.0
'@angular/material': 21.0.2
date-fns: '>2.20.0 <5.0'
'@angular/material@20.2.11': '@angular/material@20.2.11':
resolution: {integrity: sha512-lrmudo8n06o6UBVKx/slAfUIjJghwub5JZfIOmQd2js0Vz+eBJYcDJsgBB8lG/LSXKLlPkNwe6o6nJBMVBZg1g==} resolution: {integrity: sha512-lrmudo8n06o6UBVKx/slAfUIjJghwub5JZfIOmQd2js0Vz+eBJYcDJsgBB8lG/LSXKLlPkNwe6o6nJBMVBZg1g==}
peerDependencies: peerDependencies:
@@ -980,6 +996,12 @@ packages:
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
engines: {node: '>=0.1.90'} engines: {node: '>=0.1.90'}
'@date-fns/tz@1.4.1':
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
'@date-fns/utc@2.1.1':
resolution: {integrity: sha512-SlJDfG6RPeEX8wEVv6ZB3kak4MmbtyiI2qX/5zuKdordbrhB/iaJ58GVMZgJ6P1sJaM1gMgENFYYeg1JWrCFrA==}
'@discoveryjs/json-ext@0.6.3': '@discoveryjs/json-ext@0.6.3':
resolution: {integrity: sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==} resolution: {integrity: sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==}
engines: {node: '>=14.17.0'} engines: {node: '>=14.17.0'}
@@ -2862,6 +2884,9 @@ packages:
resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==}
engines: {node: '>= 14'} engines: {node: '>= 14'}
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
date-format@4.0.14: date-format@4.0.14:
resolution: {integrity: sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==} resolution: {integrity: sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
@@ -4778,10 +4803,6 @@ packages:
run-parallel@1.2.0: run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
run-script-os@1.1.6:
resolution: {integrity: sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw==}
hasBin: true
rxjs@7.8.2: rxjs@7.8.2:
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
@@ -5990,6 +6011,13 @@ snapshots:
rxjs: 7.8.2 rxjs: 7.8.2
tslib: 2.8.1 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)':
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)
date-fns: 4.1.0
tslib: 2.8.1
'@angular/material@20.2.11(437fd31793958c779574d25f524a2c50)': '@angular/material@20.2.11(437fd31793958c779574d25f524a2c50)':
dependencies: dependencies:
'@angular/cdk': 20.2.11(@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))(rxjs@7.8.2) '@angular/cdk': 20.2.11(@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))(rxjs@7.8.2)
@@ -6757,6 +6785,10 @@ snapshots:
'@colors/colors@1.5.0': {} '@colors/colors@1.5.0': {}
'@date-fns/tz@1.4.1': {}
'@date-fns/utc@2.1.1': {}
'@discoveryjs/json-ext@0.6.3': {} '@discoveryjs/json-ext@0.6.3': {}
'@electric-sql/pglite-socket@0.0.6(@electric-sql/pglite@0.3.2)': '@electric-sql/pglite-socket@0.0.6(@electric-sql/pglite@0.3.2)':
@@ -7854,7 +7886,7 @@ snapshots:
'@types/connect-history-api-fallback@1.5.4': '@types/connect-history-api-fallback@1.5.4':
dependencies: dependencies:
'@types/express-serve-static-core': 4.19.7 '@types/express-serve-static-core': 5.1.0
'@types/node': 24.10.1 '@types/node': 24.10.1
'@types/connect-pg-simple@7.0.3': '@types/connect-pg-simple@7.0.3':
@@ -7993,7 +8025,7 @@ snapshots:
'@types/serve-index@1.9.4': '@types/serve-index@1.9.4':
dependencies: dependencies:
'@types/express': 4.17.25 '@types/express': 5.0.6
'@types/serve-static@1.15.10': '@types/serve-static@1.15.10':
dependencies: dependencies:
@@ -8648,6 +8680,8 @@ snapshots:
data-uri-to-buffer@6.0.2: data-uri-to-buffer@6.0.2:
optional: true optional: true
date-fns@4.1.0: {}
date-format@4.0.14: {} date-format@4.0.14: {}
debug@2.6.9: debug@2.6.9:
@@ -10808,8 +10842,6 @@ snapshots:
dependencies: dependencies:
queue-microtask: 1.2.3 queue-microtask: 1.2.3
run-script-os@1.1.6: {}
rxjs@7.8.2: rxjs@7.8.2:
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
+2 -2
View File
@@ -19,8 +19,8 @@
"bearerFormat": "JWT" "bearerFormat": "JWT"
} }
}, },
"basePath": "/api/v1", "basePath": "",
"servers": ["localhost:8080/api/v1", "tkd-api.toni714.de/api/v1"], "servers": ["localhost:3000", "tkd-api.toni714.de"],
"info": { "info": {
"title": "Tcc Member API", "title": "Tcc Member API",
"description": "API for the TCC Member Database", "description": "API for the TCC Member Database",