import legacy frontend & fix dependency issues
This commit is contained in:
Vendored
+27
@@ -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
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 4,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"bracketSameLine": true,
|
||||||
|
"semi": true,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.yml",
|
||||||
|
"options": {
|
||||||
|
"tabWidth": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -1,53 +1,85 @@
|
|||||||
import {
|
import {
|
||||||
ApplicationConfig,
|
ApplicationConfig,
|
||||||
isDevMode,
|
LOCALE_ID,
|
||||||
provideBrowserGlobalErrorListeners,
|
provideBrowserGlobalErrorListeners,
|
||||||
provideZoneChangeDetection,
|
provideZoneChangeDetection,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
PreloadAllModules,
|
PreloadAllModules,
|
||||||
provideRouter,
|
provideRouter,
|
||||||
withComponentInputBinding,
|
withComponentInputBinding,
|
||||||
withPreloading,
|
withPreloading,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AutoRefreshTokenService,
|
AutoRefreshTokenService,
|
||||||
createInterceptorCondition,
|
createInterceptorCondition,
|
||||||
INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
|
INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
|
||||||
IncludeBearerTokenCondition,
|
IncludeBearerTokenCondition,
|
||||||
provideKeycloak,
|
provideKeycloak,
|
||||||
UserActivityService,
|
UserActivityService,
|
||||||
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(
|
||||||
provideBrowserGlobalErrorListeners(),
|
routes,
|
||||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
withPreloading(PreloadAllModules),
|
||||||
provideKeycloak({
|
withComponentInputBinding(),
|
||||||
config: {
|
),
|
||||||
url: isDevMode() ? 'http://localhost:8080/' : 'https://keycloak.toni714.de:8443/',
|
provideBrowserGlobalErrorListeners(),
|
||||||
realm: 'taekwondo',
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
clientId: 'angular-frontend',
|
provideKeycloak({
|
||||||
},
|
config: {
|
||||||
initOptions: {
|
url: true
|
||||||
onLoad: 'check-sso',
|
? 'http://localhost:8080/'
|
||||||
silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
|
: 'https://keycloak.toni714.de:8443/',
|
||||||
},
|
realm: 'taekwondo',
|
||||||
features: [withAutoRefreshToken({ sessionTimeout: 600000 })],
|
clientId: 'angular-frontend',
|
||||||
providers: [AutoRefreshTokenService, UserActivityService],
|
},
|
||||||
}),
|
initOptions: {
|
||||||
{
|
onLoad: 'check-sso',
|
||||||
provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
|
silentCheckSsoRedirectUri:
|
||||||
useValue: [
|
window.location.origin + '/silent-check-sso.html',
|
||||||
createInterceptorCondition<IncludeBearerTokenCondition>({
|
},
|
||||||
urlPattern: isDevMode()? /^http:\/\/localhost:3000.*$/ : /^https:\/\/tkd-api\.toni714\.de.*$/,
|
features: [withAutoRefreshToken({ sessionTimeout: 600000 })],
|
||||||
|
providers: [AutoRefreshTokenService, UserActivityService],
|
||||||
}),
|
}),
|
||||||
],
|
{
|
||||||
},
|
provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
|
||||||
],
|
useValue: [
|
||||||
|
createInterceptorCondition<IncludeBearerTokenCondition>({
|
||||||
|
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 },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,55 +1,74 @@
|
|||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ActivatedRouteSnapshot,
|
ActivatedRouteSnapshot,
|
||||||
CanActivateFn,
|
CanActivateFn,
|
||||||
Router,
|
Router,
|
||||||
RouterStateSnapshot,
|
RouterStateSnapshot,
|
||||||
Routes,
|
Routes,
|
||||||
UrlTree,
|
UrlTree,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
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(
|
||||||
const isAllowed = async (
|
roles: string[] = [],
|
||||||
_: ActivatedRouteSnapshot,
|
any: boolean = false,
|
||||||
__: RouterStateSnapshot,
|
): CanActivateFn {
|
||||||
authData: AuthGuardData
|
const isAllowed = async (
|
||||||
): Promise<boolean | UrlTree> => {
|
_: ActivatedRouteSnapshot,
|
||||||
const { authenticated, grantedRoles } = authData;
|
__: RouterStateSnapshot,
|
||||||
const allRoles = Object.values(grantedRoles).flat();
|
authData: AuthGuardData,
|
||||||
|
): Promise<boolean | UrlTree> => {
|
||||||
|
const { authenticated, grantedRoles } = authData;
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
const router = inject(Router);
|
const router = inject(Router);
|
||||||
return router.parseUrl('/login');
|
return router.parseUrl('/login');
|
||||||
};
|
};
|
||||||
|
|
||||||
return createAuthGuard<CanActivateFn>(isAllowed);
|
return createAuthGuard<CanActivateFn>(isAllowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{ path: 'login', component: Login },
|
{ path: 'login', component: Login },
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
loadComponent: () => import('./components/home/home').then((mod) => mod.Home),
|
loadComponent: () =>
|
||||||
canActivate: [requireRoles()],
|
import('./components/home/home').then(mod => mod.Home),
|
||||||
},
|
canActivate: [requireRoles()],
|
||||||
{
|
},
|
||||||
path: 'home',
|
{
|
||||||
loadComponent: () => import('./components/home/home').then((mod) => mod.Home),
|
path: 'home',
|
||||||
canActivate: [requireRoles()],
|
loadComponent: () =>
|
||||||
},
|
import('./components/home/home').then(mod => mod.Home),
|
||||||
{ path: '**', component: MissingPage },
|
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 },
|
||||||
];
|
];
|
||||||
|
|||||||
+13
@@ -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ü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>
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
form{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
form > *{
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
+23
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
+57
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
@@ -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>
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
form{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
form > *{
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
+22
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
+47
@@ -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ü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 ‑ "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(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,91 +1,127 @@
|
|||||||
import { computed, effect, inject, Injectable, signal } from '@angular/core';
|
import { computed, effect, inject, Injectable, signal } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
KEYCLOAK_EVENT_SIGNAL,
|
KEYCLOAK_EVENT_SIGNAL,
|
||||||
KeycloakEventType,
|
KeycloakEventType,
|
||||||
ReadyArgs,
|
ReadyArgs,
|
||||||
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,
|
||||||
Unauthenticated,
|
Unauthenticated,
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
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>(
|
||||||
private readonly _userInfo = signal<KeycloakProfile | null>(null);
|
AuthenticationState.Unknown,
|
||||||
public readonly authenticationState = this._authenticationState.asReadonly();
|
);
|
||||||
public readonly loggedIn = computed(()=>{
|
private readonly _userInfo = signal<KeycloakProfile | null>(null);
|
||||||
return this.authenticationState() === AuthenticationState.Authenticated;
|
private readonly _claims = signal<Record<Claim, boolean> | null>(null);
|
||||||
});
|
public readonly authenticationState =
|
||||||
public readonly userInfo = this._userInfo.asReadonly();
|
this._authenticationState.asReadonly();
|
||||||
|
public readonly loggedIn = computed(() => {
|
||||||
public constructor() {
|
return this.authenticationState() === AuthenticationState.Authenticated;
|
||||||
effect(() => {
|
|
||||||
const event = this.keycloakSignal();
|
|
||||||
switch (event?.type) {
|
|
||||||
case KeycloakEventType.Ready:
|
|
||||||
this._authenticationState.set(
|
|
||||||
typeEventArgs<ReadyArgs>(event.args)
|
|
||||||
? AuthenticationState.Authenticated
|
|
||||||
: AuthenticationState.Unauthenticated
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case KeycloakEventType.AuthSuccess:
|
|
||||||
this._authenticationState.set(AuthenticationState.Authenticated);
|
|
||||||
break;
|
|
||||||
case KeycloakEventType.AuthLogout:
|
|
||||||
this._authenticationState.set(AuthenticationState.Unauthenticated);
|
|
||||||
break;
|
|
||||||
case KeycloakEventType.AuthError:
|
|
||||||
this._authenticationState.set(AuthenticationState.Unauthenticated);
|
|
||||||
break;
|
|
||||||
case KeycloakEventType.AuthRefreshError:
|
|
||||||
this._authenticationState.set(AuthenticationState.Unauthenticated);
|
|
||||||
break;
|
|
||||||
case KeycloakEventType.AuthRefreshSuccess:
|
|
||||||
this._authenticationState.set(AuthenticationState.Authenticated);
|
|
||||||
break;
|
|
||||||
case KeycloakEventType.TokenExpired:
|
|
||||||
this._authenticationState.set(AuthenticationState.Unauthenticated);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
public readonly userInfo = this._userInfo.asReadonly();
|
||||||
|
public readonly claims = this._claims.asReadonly();
|
||||||
|
|
||||||
effect(async () => {
|
public constructor() {
|
||||||
const authenticationState = this.authenticationState();
|
effect(() => {
|
||||||
if (authenticationState !== AuthenticationState.Authenticated) {
|
const event = this.keycloakSignal();
|
||||||
this._userInfo.set(null);
|
|
||||||
}
|
|
||||||
const profile = await this.keycloak.loadUserProfile();
|
|
||||||
this._userInfo.set(profile);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async login(location?: string): Promise<void> {
|
apiDefaults.headers['Authorization'] = this.keycloak.token
|
||||||
if(this._authenticationState() === AuthenticationState.Authenticated){
|
? `Bearer ${this.keycloak.token}`
|
||||||
return;
|
: undefined;
|
||||||
|
|
||||||
|
switch (event?.type) {
|
||||||
|
case KeycloakEventType.Ready:
|
||||||
|
this._authenticationState.set(
|
||||||
|
typeEventArgs<ReadyArgs>(event.args)
|
||||||
|
? AuthenticationState.Authenticated
|
||||||
|
: AuthenticationState.Unauthenticated,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case KeycloakEventType.AuthSuccess:
|
||||||
|
this._authenticationState.set(
|
||||||
|
AuthenticationState.Authenticated,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case KeycloakEventType.AuthLogout:
|
||||||
|
this._authenticationState.set(
|
||||||
|
AuthenticationState.Unauthenticated,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case KeycloakEventType.AuthError:
|
||||||
|
this._authenticationState.set(
|
||||||
|
AuthenticationState.Unauthenticated,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case KeycloakEventType.AuthRefreshError:
|
||||||
|
this._authenticationState.set(
|
||||||
|
AuthenticationState.Unauthenticated,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case KeycloakEventType.AuthRefreshSuccess:
|
||||||
|
this._authenticationState.set(
|
||||||
|
AuthenticationState.Authenticated,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case KeycloakEventType.TokenExpired:
|
||||||
|
this._authenticationState.set(
|
||||||
|
AuthenticationState.Unauthenticated,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
effect(async () => {
|
||||||
|
const authenticationState = this.authenticationState();
|
||||||
|
if (authenticationState !== AuthenticationState.Authenticated) {
|
||||||
|
this._userInfo.set(null);
|
||||||
|
}
|
||||||
|
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._claims.set(claims as Record<Claim, boolean>);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var redirectUri = location
|
public async login(location?: string): Promise<void> {
|
||||||
? `${window.location.origin}/${location}`
|
if (this._authenticationState() === AuthenticationState.Authenticated) {
|
||||||
: `${window.location.origin}/login`;
|
return;
|
||||||
return await this.keycloak.login({
|
}
|
||||||
redirectUri: redirectUri,
|
|
||||||
locale: 'de-DE',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async logout(): Promise<void> {
|
var redirectUri = location
|
||||||
return await this.keycloak.logout({ redirectUri: window.location.origin + '/login' });
|
? `${window.location.origin}/${location}`
|
||||||
}
|
: `${window.location.origin}/login`;
|
||||||
|
return await this.keycloak.login({
|
||||||
|
redirectUri: redirectUri,
|
||||||
|
locale: 'de-DE',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async logout(): Promise<void> {
|
||||||
|
return await this.keycloak.logout({
|
||||||
|
redirectUri: window.location.origin + '/login',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
Generated
+54
-22
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user