dev env setup
This commit is contained in:
@@ -3,3 +3,5 @@ generated/
|
|||||||
/prisma/src/generated/prisma
|
/prisma/src/generated/prisma
|
||||||
|
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
keycloak/realm-export
|
||||||
|
|||||||
@@ -2,5 +2,13 @@
|
|||||||
"tabWidth": 4,
|
"tabWidth": 4,
|
||||||
"arrowParens": "avoid",
|
"arrowParens": "avoid",
|
||||||
"bracketSameLine": true,
|
"bracketSameLine": true,
|
||||||
"semi": true
|
"semi": true,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.yml",
|
||||||
|
"options": {
|
||||||
|
"tabWidth": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./postgres.DOCKERFILE
|
||||||
|
container_name: dev-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: devpassword
|
||||||
|
POSTGRES_INITDB_ARGS: "--locale=de_DE.utf8 --lc-collate=de_DE.utf8 --lc-ctype=de_DE.utf8"
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql
|
||||||
|
- ./postgres/initdb:/docker-entrypoint-initdb.d/
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
keycloak:
|
||||||
|
image: quay.io/keycloak/keycloak:26.4
|
||||||
|
container_name: dev-keycloak
|
||||||
|
command:
|
||||||
|
- start-dev
|
||||||
|
- --import-realm
|
||||||
|
environment:
|
||||||
|
KC_HOSTNAME: localhost
|
||||||
|
KC_DB: postgres
|
||||||
|
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
|
||||||
|
KC_DB_USERNAME: keycloak
|
||||||
|
KC_DB_PASSWORD: devpassword
|
||||||
|
KC_BOOTSTRAP_ADMIN_USERNAME: admin
|
||||||
|
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- ./keycloak/realm-export:/opt/keycloak/data/import
|
||||||
|
- keycloak_data:/opt/keycloak/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
keycloak_data:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
FROM postgres:18
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y locales \
|
||||||
|
&& sed -i '/de_DE.UTF-8/s/^# //g' /etc/locale.gen \
|
||||||
|
&& locale-gen
|
||||||
|
|
||||||
|
ENV LANG=de_DE.utf8
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE ROLE webapi WITH
|
||||||
|
LOGIN
|
||||||
|
NOSUPERUSER
|
||||||
|
INHERIT
|
||||||
|
NOCREATEDB
|
||||||
|
NOCREATEROLE
|
||||||
|
NOREPLICATION
|
||||||
|
NOBYPASSRLS
|
||||||
|
PASSWORD 'devpassword';
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
CREATE ROLE keycloak WITH
|
||||||
|
LOGIN
|
||||||
|
NOSUPERUSER
|
||||||
|
INHERIT
|
||||||
|
NOCREATEDB
|
||||||
|
NOCREATEROLE
|
||||||
|
NOREPLICATION
|
||||||
|
NOBYPASSRLS
|
||||||
|
PASSWORD 'devpassword';
|
||||||
|
|
||||||
|
CREATE DATABASE keycloak
|
||||||
|
WITH
|
||||||
|
OWNER = keycloak
|
||||||
|
ENCODING = 'UTF8'
|
||||||
|
TABLESPACE = pg_default
|
||||||
|
CONNECTION LIMIT = -1
|
||||||
|
IS_TEMPLATE = False;
|
||||||
|
|
||||||
|
-- TODO
|
||||||
|
-- Replace Client Secret in realm-export.json
|
||||||
|
-- replace Frontend redirect URLS in realm-export.json
|
||||||
|
-- SET NODE_ENV=production when running on server
|
||||||
|
-- SET DEFAULT_PERSISSIONS for webapi user
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE ROLE prisma WITH
|
||||||
|
LOGIN
|
||||||
|
NOSUPERUSER
|
||||||
|
INHERIT
|
||||||
|
CREATEDB
|
||||||
|
NOCREATEROLE
|
||||||
|
NOREPLICATION
|
||||||
|
NOBYPASSRLS
|
||||||
|
PASSWORD 'devpassword';
|
||||||
+16
-10
@@ -3,29 +3,35 @@
|
|||||||
"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/connect-pg-simple": "^7.0.3",
|
||||||
"@types/express": "^5.0.5",
|
"@types/cors": "^2.8.19",
|
||||||
|
"@types/express": "^5.0.6",
|
||||||
"@types/express-session": "^1.18.2",
|
"@types/express-session": "^1.18.2",
|
||||||
"@types/node": "^24.9.2",
|
"@types/node": "^24.10.1",
|
||||||
"@types/pg": "^8.15.6",
|
"@types/pg": "^8.15.6",
|
||||||
"prisma": "^7.0.1",
|
"dotenv": "^17.2.3",
|
||||||
"tsx": "^4.20.6",
|
"prisma": "^7.1.0",
|
||||||
|
"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.4",
|
"@keycloak/keycloak-admin-client": "^26.4.7",
|
||||||
"@prisma/adapter-pg": "^7.0.1",
|
"@prisma/adapter-pg": "^7.1.0",
|
||||||
"@prisma/client": "^7.0.1",
|
"@prisma/client": "^7.1.0",
|
||||||
"@tsoa/runtime": "^6.6.0",
|
"@tsoa/runtime": "^6.6.0",
|
||||||
"connect-pg-simple": "^10.0.0",
|
"connect-pg-simple": "^10.0.0",
|
||||||
"dotenv": "^17.2.3",
|
"cors": "^2.8.5",
|
||||||
"express": "^5.1.0",
|
"express": "^5.2.1",
|
||||||
"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",
|
||||||
|
|||||||
@@ -8,21 +8,29 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Member {
|
model Member {
|
||||||
id String @db.Uuid @id @default(dbgenerated("uuidv7()"))
|
id String @id @default(dbgenerated("uuidv7()")) @db.Uuid
|
||||||
vorname String? @db.VarChar
|
vorname String? @db.VarChar
|
||||||
nachname String? @db.VarChar
|
nachname String? @db.VarChar
|
||||||
geburtsdatum DateTime? @db.Date
|
geburtsdatum DateTime? @db.Date
|
||||||
geburtsort String? @db.VarChar
|
geburtsort String? @db.VarChar
|
||||||
geschlecht String? @db.VarChar
|
geschlecht String? @db.VarChar
|
||||||
passnummer String? @db.VarChar
|
passnummer String? @db.VarChar
|
||||||
uvertrag Boolean? @db.Boolean
|
uvertrag Boolean?
|
||||||
verein Boolean? @db.Boolean
|
verein Boolean?
|
||||||
graduierung String? @db.VarChar
|
graduierung String? @db.VarChar
|
||||||
letztePruefung DateTime? @db.Date
|
letztePruefung DateTime? @db.Date
|
||||||
kontakt String? @db.VarChar
|
kontakt String? @db.VarChar
|
||||||
kuendigungzum DateTime? @db.Date
|
kuendigungzum DateTime? @db.Date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model session {
|
||||||
|
sid String @id @db.VarChar
|
||||||
|
sess Json @db.Json
|
||||||
|
expire DateTime @db.Timestamp(6)
|
||||||
|
|
||||||
|
@@index([expire], map: "IDX_session_expire")
|
||||||
|
}
|
||||||
|
|
||||||
enum Topic {
|
enum Topic {
|
||||||
TEST
|
TEST
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Starting development environment..."
|
||||||
|
|
||||||
|
pnpm -v
|
||||||
@@ -17,7 +17,9 @@ export class HealthCheckController extends Controller {
|
|||||||
|
|
||||||
@Get("ok")
|
@Get("ok")
|
||||||
@SuccessResponse("200", "OK")
|
@SuccessResponse("200", "OK")
|
||||||
public async getOk() {}
|
public async getOk() {
|
||||||
|
return "ok";
|
||||||
|
}
|
||||||
|
|
||||||
@Get("auth")
|
@Get("auth")
|
||||||
@Security(KC_SECURITY_NAME)
|
@Security(KC_SECURITY_NAME)
|
||||||
@@ -25,6 +27,7 @@ export class HealthCheckController extends Controller {
|
|||||||
public async getAuth() {}
|
public async getAuth() {}
|
||||||
|
|
||||||
@Post("testmessage")
|
@Post("testmessage")
|
||||||
|
@SuccessResponse("200", "OK")
|
||||||
public async sendTestMessage() {
|
public async sendTestMessage() {
|
||||||
await this.messaging.sendTestMessage();
|
await this.messaging.sendTestMessage();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import {
|
|||||||
Path,
|
Path,
|
||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
|
Request,
|
||||||
Response,
|
Response,
|
||||||
Route,
|
Route,
|
||||||
Security,
|
Security,
|
||||||
SuccessResponse,
|
SuccessResponse,
|
||||||
} from "@tsoa/runtime";
|
} from "@tsoa/runtime";
|
||||||
|
import express from "express";
|
||||||
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 { Claim } from "../models/claim.js";
|
||||||
@@ -22,19 +24,27 @@ import { MembersService } from "../services/membersService.js";
|
|||||||
export class MembersContoller extends Controller {
|
export class MembersContoller extends Controller {
|
||||||
private readonly membersService = inject(MembersService);
|
private readonly membersService = inject(MembersService);
|
||||||
@Security(KC_SECURITY_NAME, [Claim.MemberAdmin])
|
@Security(KC_SECURITY_NAME, [Claim.MemberAdmin])
|
||||||
|
@SuccessResponse("200", "OK")
|
||||||
@Get()
|
@Get()
|
||||||
public async getMembers(): Promise<Member[]> {
|
public async getMembers(
|
||||||
return await this.membersService.getMembers();
|
@Request() request: express.Request,
|
||||||
|
): Promise<Member[]> {
|
||||||
|
return await this.membersService.getMembers(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Security(KC_SECURITY_NAME, [Claim.MemberAdmin])
|
@Security(KC_SECURITY_NAME, [Claim.MemberAdmin])
|
||||||
@SuccessResponse("200", "OK")
|
@SuccessResponse("200", "OK")
|
||||||
@Response(404, "Member not found")
|
@Response(404, "Member not found")
|
||||||
@Get("{memberId}")
|
@Get("{memberId}")
|
||||||
public async getMember(@Path() memberId: string): Promise<Member> {
|
public async getMember(
|
||||||
|
@Path() memberId: string,
|
||||||
|
@Request() request: express.Request,
|
||||||
|
): Promise<Member> {
|
||||||
try {
|
try {
|
||||||
const foundMember =
|
const foundMember = await this.membersService.getMemberById(
|
||||||
await this.membersService.getMemberById(memberId);
|
memberId,
|
||||||
|
request,
|
||||||
|
);
|
||||||
return foundMember;
|
return foundMember;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (
|
if (
|
||||||
@@ -53,8 +63,9 @@ export class MembersContoller extends Controller {
|
|||||||
@Post()
|
@Post()
|
||||||
public async createMember(
|
public async createMember(
|
||||||
@Body() createArgs: MemberCreateArgs,
|
@Body() createArgs: MemberCreateArgs,
|
||||||
|
@Request() request: express.Request,
|
||||||
): Promise<Member> {
|
): Promise<Member> {
|
||||||
return await this.membersService.createMember(createArgs);
|
return await this.membersService.createMember(createArgs, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Security(KC_SECURITY_NAME, [Claim.MemberAdmin])
|
@Security(KC_SECURITY_NAME, [Claim.MemberAdmin])
|
||||||
@@ -62,9 +73,12 @@ export class MembersContoller extends Controller {
|
|||||||
@Response(400, "Bad Request")
|
@Response(400, "Bad Request")
|
||||||
@Response(404, "Member not found")
|
@Response(404, "Member not found")
|
||||||
@Put()
|
@Put()
|
||||||
public async updateMember(@Body() member: Member): Promise<void> {
|
public async updateMember(
|
||||||
|
@Body() member: Member,
|
||||||
|
@Request() request: express.Request,
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.membersService.updateMember(member);
|
await this.membersService.updateMember(member, request);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (
|
if (
|
||||||
error instanceof Error &&
|
error instanceof Error &&
|
||||||
@@ -83,8 +97,13 @@ export class MembersContoller extends Controller {
|
|||||||
@Delete("{memberId}")
|
@Delete("{memberId}")
|
||||||
public async scheduleDeletion(
|
public async scheduleDeletion(
|
||||||
@Path() memberId: string,
|
@Path() memberId: string,
|
||||||
@Body() cancelDate: Date
|
@Body() cancelDate: Date,
|
||||||
){
|
@Request() request: express.Request,
|
||||||
await this.membersService.scheduleDeletion(memberId, cancelDate);
|
) {
|
||||||
|
await this.membersService.scheduleDeletion(
|
||||||
|
memberId,
|
||||||
|
cancelDate,
|
||||||
|
request,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
Body,
|
|
||||||
BodyProp,
|
BodyProp,
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
@@ -8,25 +7,27 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
Route,
|
Route,
|
||||||
Security,
|
Security,
|
||||||
|
SuccessResponse,
|
||||||
} from "@tsoa/runtime";
|
} from "@tsoa/runtime";
|
||||||
import { inject } from "../infrastructure/di";
|
import { inject } from "../infrastructure/di/index.js";
|
||||||
import { Claim } from "../models/claim";
|
import { Claim } from "../models/claim.js";
|
||||||
import { Topic } from "../models/topic";
|
import { type User } from "../models/user.js";
|
||||||
import { User } from "../models/user";
|
|
||||||
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";
|
import { UserService } from "../services/userService.js";
|
||||||
|
|
||||||
@Route("users")
|
@Route("users")
|
||||||
export class UsersController extends Controller {
|
export class UsersController extends Controller {
|
||||||
private readonly userService = inject(UserService);
|
private readonly userService = inject(UserService);
|
||||||
|
|
||||||
@Security(KC_SECURITY_NAME, [Claim.UserAdmin])
|
@Security(KC_SECURITY_NAME, [Claim.UserAdmin])
|
||||||
|
@SuccessResponse("200", "OK")
|
||||||
@Get()
|
@Get()
|
||||||
public async getUsers(): Promise<User[]> {
|
public async getUsers(): Promise<User[]> {
|
||||||
return await this.userService.getUsers();
|
return await this.userService.getUsers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Security(KC_SECURITY_NAME, [Claim.UserAdmin])
|
@Security(KC_SECURITY_NAME, [Claim.UserAdmin])
|
||||||
|
@SuccessResponse("200", "OK")
|
||||||
@Put("/claims/{uid}")
|
@Put("/claims/{uid}")
|
||||||
public async setClaims(
|
public async setClaims(
|
||||||
@Path() uid: string,
|
@Path() uid: string,
|
||||||
@@ -36,12 +37,12 @@ export class UsersController extends Controller {
|
|||||||
await this.userService.setClaim(uid, claim, value);
|
await this.userService.setClaim(uid, claim, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Security(KC_SECURITY_NAME)
|
// @Security(KC_SECURITY_NAME)
|
||||||
@Put("/subscriptions/{token}")
|
// @Put("/subscriptions/{token}")
|
||||||
public async setSubscriptions(
|
// public async setSubscriptions(
|
||||||
@Body() topics: Topic[],
|
// @Body() topics: Topic[],
|
||||||
@Path() token: string,
|
// @Path() token: string,
|
||||||
) {
|
// ) {
|
||||||
await this.userService.setSubscriptions(token, topics);
|
// await this.userService.setSubscriptions(token, topics);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,66 @@
|
|||||||
import express from "express";
|
import cors from "cors";
|
||||||
|
import express, {
|
||||||
|
type NextFunction,
|
||||||
|
type Request,
|
||||||
|
type Response,
|
||||||
|
} from "express";
|
||||||
import { RegisterRoutes } from "./generated/routes.js";
|
import { RegisterRoutes } from "./generated/routes.js";
|
||||||
|
import { inject } from "./infrastructure/di/injector.js";
|
||||||
import { sessionHandler } from "./infrastructure/sessionHandler.js";
|
import { sessionHandler } from "./infrastructure/sessionHandler.js";
|
||||||
|
import { EnvironmentService } from "./services/environmentService.js";
|
||||||
import { keycloak } from "./services/keycloak/user/index.js";
|
import { keycloak } from "./services/keycloak/user/index.js";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = 3000;
|
|
||||||
|
|
||||||
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());
|
||||||
|
app.use(
|
||||||
|
cors({
|
||||||
|
origin: [
|
||||||
|
"https://taekwondo-chemnitz.toni714.de",
|
||||||
|
"https://tcc-1-ev-intern.web.app",
|
||||||
|
"http://localhost:4200",
|
||||||
|
"https://localhost:4200",
|
||||||
|
],
|
||||||
|
//TODO reevaluate
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// const dbService = inject(DatabaseService);
|
||||||
|
// app.use(async (req, res, next) => {
|
||||||
|
// await dbService.createTransaction(async prisma => {
|
||||||
|
// (req as any).transaction = prisma;
|
||||||
|
// return new Promise<void>((resolve, reject) => {
|
||||||
|
// res.on("finish", resolve);
|
||||||
|
// res.on("close", resolve);
|
||||||
|
// res.on("error", reject);
|
||||||
|
// next();
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
RegisterRoutes(app);
|
RegisterRoutes(app);
|
||||||
|
|
||||||
app.listen(port, () => {
|
const environment = inject(EnvironmentService);
|
||||||
console.log(`Server is running at http://localhost:${port}`);
|
// wants to be last
|
||||||
|
app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => {
|
||||||
|
res.status(500);
|
||||||
|
res.send(JSON.stringify(err));
|
||||||
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (environment.isDev()) {
|
||||||
|
//TODO create self-signed for localhost
|
||||||
|
const port = 3000;
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Server is running at http://localhost:${port}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
//TODO start with HTTPS in production
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("done");
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { currentInjector } from "./injector.js";
|
||||||
import { type Token } from "./types.js";
|
import { type Token } from "./types.js";
|
||||||
|
|
||||||
const INJECTABLE_META = Symbol("injectable");
|
const INJECTABLE_META = Symbol("injectable");
|
||||||
const INJECTABLE_REGISTRY = new Set<Token>();
|
|
||||||
|
|
||||||
export function Injectable(): ClassDecorator {
|
export function Injectable(): ClassDecorator {
|
||||||
return function (target: any) {
|
return function (target: any) {
|
||||||
@@ -10,14 +10,10 @@ export function Injectable(): ClassDecorator {
|
|||||||
writable: false,
|
writable: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
INJECTABLE_REGISTRY.add(target);
|
currentInjector.add(target as Token);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isInjectable(targert: any) {
|
export function isInjectable(targert: any) {
|
||||||
return !!targert[INJECTABLE_META];
|
return !!targert[INJECTABLE_META];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllInjectables(): Token[] {
|
|
||||||
return Array.from(INJECTABLE_REGISTRY);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
import { getAllInjectables, isInjectable } from "./injectable-decorator.js";
|
import { type Token } from "./types.js";
|
||||||
import { type Provider, type Token } from "./types.js";
|
|
||||||
|
|
||||||
const currentInjector: Injector = createGlobalInjector();
|
|
||||||
|
|
||||||
export function inject<T>(token: Token<T>): T {
|
|
||||||
if (!currentInjector) {
|
|
||||||
throw new Error("inject() called outside of injection context");
|
|
||||||
}
|
|
||||||
return currentInjector.get(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Injector {
|
export class Injector {
|
||||||
private instances = new Map<Token, any>();
|
private instances = new Map<Token, any>();
|
||||||
private records = new Map<Token, Provider>();
|
private records = new Set<Token>();
|
||||||
|
|
||||||
constructor(providers: Provider[]) {
|
add(token: Token) {
|
||||||
providers.forEach(p => this.records.set(p.provide, p));
|
this.records.add(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
get<T>(token: Token<T>): T {
|
get<T>(token: Token<T>): T {
|
||||||
@@ -23,8 +13,7 @@ export class Injector {
|
|||||||
return this.instances.get(token);
|
return this.instances.get(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = this.records.get(token);
|
if (!this.records.has(token)) {
|
||||||
if (!provider) {
|
|
||||||
throw new Error("No provider for token");
|
throw new Error("No provider for token");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,19 +27,11 @@ export class Injector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createInjector(injectables: Token[]) {
|
export const currentInjector: Injector = new Injector();
|
||||||
const providers: Provider[] = injectables.map(injectable => {
|
|
||||||
if (!isInjectable(injectable)) {
|
export function inject<T>(token: Token<T>): T {
|
||||||
throw new Error(`${injectable.name} is not injectable`);
|
if (!currentInjector) {
|
||||||
|
throw new Error("inject() called outside of injection context");
|
||||||
}
|
}
|
||||||
return { provide: injectable, useClass: injectable };
|
return currentInjector.get(token);
|
||||||
});
|
|
||||||
|
|
||||||
return new Injector(providers);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createGlobalInjector() {
|
|
||||||
const injectables = getAllInjectables();
|
|
||||||
|
|
||||||
return createInjector(injectables);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1 @@
|
|||||||
export type Token<T = any> = new (...args: any[]) => T;
|
export type Token<T = any> = new (...args: any[]) => T;
|
||||||
export interface Provider<T = any> {
|
|
||||||
provide: Token<T>;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ async function doKeycloakAuth(
|
|||||||
roles?: string[],
|
roles?: string[],
|
||||||
response?: Response,
|
response?: Response,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
if (roles) {
|
if (roles && roles.length > 0) {
|
||||||
return await promisifyMiddleware(
|
return await promisifyMiddleware(
|
||||||
request,
|
request,
|
||||||
keycloak.enforcer(roles?.map(role => `realm:${role}`)),
|
keycloak.enforcer(roles?.map(role => `realm:${role}`)),
|
||||||
@@ -35,7 +35,6 @@ export async function expressAuthentication(
|
|||||||
roles?: string[],
|
roles?: string[],
|
||||||
response?: Response,
|
response?: Response,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
console.dir({ request, securityName, roles });
|
|
||||||
switch (securityName) {
|
switch (securityName) {
|
||||||
case KC_SECURITY_NAME:
|
case KC_SECURITY_NAME:
|
||||||
return doKeycloakAuth(request, roles, response);
|
return doKeycloakAuth(request, roles, response);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Claim } from "./claim";
|
import { Claim } from "./claim.js";
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
readonly id?: string;
|
readonly id?: string;
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { PrismaPg } from "@prisma/adapter-pg";
|
import { PrismaPg } from "@prisma/adapter-pg";
|
||||||
|
import { type Request } from "express";
|
||||||
import { PrismaClient } from "../../generated/prisma/client.js";
|
import { PrismaClient } from "../../generated/prisma/client.js";
|
||||||
|
import { Injectable } from "../../infrastructure/di/injectable-decorator.js";
|
||||||
|
|
||||||
type ExtendedClient = ReturnType<typeof createExtendedClient>;
|
type ExtendedClient = ReturnType<typeof createExtendedClient>;
|
||||||
|
type TransactionClient = Omit<
|
||||||
|
ExtendedClient,
|
||||||
|
"$connect" | "$disconnect" | "$on" | "$transaction" | "$extends"
|
||||||
|
>;
|
||||||
|
|
||||||
export type SafeClient = Omit<
|
export type SafeClient = Omit<
|
||||||
ExtendedClient,
|
ExtendedClient,
|
||||||
@@ -33,9 +39,9 @@ function createExtendedClient(client: PrismaClient) {
|
|||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
export class DatabaseService {
|
export class DatabaseService {
|
||||||
private readonly prismaClient: ExtendedClient;
|
private readonly prismaClient: ExtendedClient;
|
||||||
private readonly safeClient: SafeClient;
|
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
const dbUser = process.env["TKD_DB_USER"];
|
const dbUser = process.env["TKD_DB_USER"];
|
||||||
@@ -46,17 +52,19 @@ export class DatabaseService {
|
|||||||
|
|
||||||
const adapter = new PrismaPg({ connectionString });
|
const adapter = new PrismaPg({ connectionString });
|
||||||
this.prismaClient = createExtendedClient(new PrismaClient({ adapter }));
|
this.prismaClient = createExtendedClient(new PrismaClient({ adapter }));
|
||||||
this.safeClient = this.prismaClient as SafeClient;
|
|
||||||
|
|
||||||
// this.prismaClient.$connect();
|
|
||||||
// this.prismaClient.$disconnect();
|
|
||||||
// this.prismaClient.$on();
|
|
||||||
// this.prismaClient.$transaction();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async doRequest<T>(
|
public async doRequest<T>(
|
||||||
request: (prisma: SafeClient) => Promise<T>,
|
command: (prisma: SafeClient) => Promise<T>,
|
||||||
|
request: Request,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
return await request(this.safeClient);
|
const transaction = (request as any).transaction as SafeClient;
|
||||||
|
return await command(transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createTransaction(
|
||||||
|
action: (prisma: TransactionClient) => Promise<void>,
|
||||||
|
) {
|
||||||
|
await this.prismaClient.$transaction(action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { Injectable } from "../infrastructure/di/index.js";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EnvironmentService {
|
||||||
|
public isDev(): boolean {
|
||||||
|
return process.env["NODE_ENV"] !== "production";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
|
import { Injectable } from "../../../infrastructure/di/injectable-decorator.js";
|
||||||
import { Injectable } from "../../../infrastructure/di/injectable-decorator";
|
|
||||||
import kcClientConfig from "./nodejs-api.json" with { type: "json" };
|
import kcClientConfig from "./nodejs-api.json" with { type: "json" };
|
||||||
|
|
||||||
import KeycloakAdminClient from "@keycloak/keycloak-admin-client";
|
import KeycloakAdminClient from "@keycloak/keycloak-admin-client";
|
||||||
import { Claim } from "../../../models/claim";
|
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation.js";
|
||||||
import { User } from "../../../models/user";
|
import { Claim } from "../../../models/claim.js";
|
||||||
|
import { type User } from "../../../models/user.js";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class KeycloakAdmin {
|
export class KeycloakAdmin {
|
||||||
@@ -54,7 +54,7 @@ export class KeycloakAdmin {
|
|||||||
const client = await this.kcAdminClient;
|
const client = await this.kcAdminClient;
|
||||||
const kcUsers = await client.users.find();
|
const kcUsers = await client.users.find();
|
||||||
return kcUsers.map(kcUser => ({
|
return kcUsers.map(kcUser => ({
|
||||||
id: kcUser.id,
|
id: kcUser.id ?? "", //TODO handle undefined id
|
||||||
username: kcUser.username || "",
|
username: kcUser.username || "",
|
||||||
roles: (kcUser.realmRoles || []).map(role => role as Claim),
|
roles: (kcUser.realmRoles || []).map(role => role as Claim),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
"clientId": "nodejs-api",
|
"clientId": "nodejs-api",
|
||||||
"name": "NodejsApi",
|
"name": "NodejsApi",
|
||||||
"description": "",
|
"description": "",
|
||||||
"rootUrl": "https://tkd-api.toni714.de",
|
"rootUrl": "http://localhost:3000",
|
||||||
"adminUrl": "https://tkd-api.toni714.de",
|
"adminUrl": "http://localhost:3000",
|
||||||
"baseUrl": "https://tkd-api.toni714.de",
|
"baseUrl": "http://localhost:3000",
|
||||||
"surrogateAuthRequired": false,
|
"surrogateAuthRequired": false,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
"clientAuthenticatorType": "client-secret",
|
"clientAuthenticatorType": "client-secret",
|
||||||
"secret": "btdeoaq2mgtoa7SpCEL1phLYcw2gGhGp",
|
"secret": "devsecret",
|
||||||
"redirectUris": ["https://tkd-api.toni714.de/*"],
|
"redirectUris": ["http://localhost:3000/*"],
|
||||||
"webOrigins": ["https://tkd-api.toni714.de"],
|
"webOrigins": ["http://localhost:3000"],
|
||||||
"notBefore": 0,
|
"notBefore": 0,
|
||||||
"bearerOnly": false,
|
"bearerOnly": false,
|
||||||
"consentRequired": false,
|
"consentRequired": false,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"realm": "taekwondo",
|
"realm": "taekwondo",
|
||||||
"auth-server-url": "https://keycloak.toni714.de:8443",
|
"auth-server-url": "https://localhost:8080",
|
||||||
"ssl-required": "external",
|
"ssl-required": "external",
|
||||||
"resource": "nodejs-api",
|
"resource": "nodejs-api",
|
||||||
"credentials": {
|
"credentials": {
|
||||||
"secret": "btdeoaq2mgtoa7SpCEL1phLYcw2gGhGp"
|
"secret": "devsecret"
|
||||||
},
|
},
|
||||||
"confidential-port": 0
|
"confidential-port": 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import express from "express";
|
||||||
import { inject, Injectable } from "../infrastructure/di/index.js";
|
import { inject, Injectable } from "../infrastructure/di/index.js";
|
||||||
import { ErrorStates } from "../infrastructure/errorStates.js";
|
import { ErrorStates } from "../infrastructure/errorStates.js";
|
||||||
import {
|
import {
|
||||||
@@ -11,16 +12,22 @@ import { DatabaseService } from "./db/prisma.js";
|
|||||||
export class MembersService {
|
export class MembersService {
|
||||||
private readonly database = inject(DatabaseService);
|
private readonly database = inject(DatabaseService);
|
||||||
|
|
||||||
public async getMembers(): Promise<Member[]> {
|
public async getMembers(request: express.Request): Promise<Member[]> {
|
||||||
const dbMembers = await this.database.doRequest(prisma =>
|
const dbMembers = await this.database.doRequest(
|
||||||
prisma.member.findMany(),
|
async prisma => await prisma.member.findMany(),
|
||||||
|
request,
|
||||||
);
|
);
|
||||||
return dbMembers.map(mapDbMemberToMember);
|
return dbMembers.map(mapDbMemberToMember);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMemberById(memberId: string): Promise<Member> {
|
public async getMemberById(
|
||||||
const member = await this.database.doRequest(prisma =>
|
memberId: string,
|
||||||
prisma.member.findFirst({ where: { id: memberId } }),
|
request: express.Request,
|
||||||
|
): Promise<Member> {
|
||||||
|
const member = await this.database.doRequest(
|
||||||
|
async prisma =>
|
||||||
|
await prisma.member.findFirst({ where: { id: memberId } }),
|
||||||
|
request,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!member) {
|
if (!member) {
|
||||||
@@ -32,36 +39,51 @@ export class MembersService {
|
|||||||
return mapDbMemberToMember(member);
|
return mapDbMemberToMember(member);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createMember(createArgs: MemberCreateArgs): Promise<Member> {
|
public async createMember(
|
||||||
const newMember = await this.database.doRequest(prisma =>
|
createArgs: MemberCreateArgs,
|
||||||
prisma.member.create({ data: createArgs }),
|
request: express.Request,
|
||||||
|
): Promise<Member> {
|
||||||
|
const newMember = await this.database.doRequest(
|
||||||
|
async prisma => await prisma.member.create({ data: createArgs }),
|
||||||
|
request,
|
||||||
);
|
);
|
||||||
return mapDbMemberToMember(newMember);
|
return mapDbMemberToMember(newMember);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateMember(member: Member): Promise<Member> {
|
public async updateMember(
|
||||||
|
member: Member,
|
||||||
|
request: express.Request,
|
||||||
|
): Promise<Member> {
|
||||||
const id = member.id;
|
const id = member.id;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
throw new Error("Member id is required for update", {
|
throw new Error("Member id is required for update", {
|
||||||
cause: ErrorStates.BAD_REQUEST,
|
cause: ErrorStates.BAD_REQUEST,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const modifiedMember = await this.database.doRequest(prisma =>
|
const modifiedMember = await this.database.doRequest(
|
||||||
prisma.member.update({
|
async prisma =>
|
||||||
|
await prisma.member.update({
|
||||||
data: { ...member, id: id },
|
data: { ...member, id: id },
|
||||||
where: { id: id },
|
where: { id: id },
|
||||||
}),
|
}),
|
||||||
|
request,
|
||||||
);
|
);
|
||||||
|
|
||||||
return mapDbMemberToMember(modifiedMember);
|
return mapDbMemberToMember(modifiedMember);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async scheduleDeletion(memberId: string, cancelDate: Date) {
|
public async scheduleDeletion(
|
||||||
await this.database.doRequest(prisma =>
|
memberId: string,
|
||||||
|
cancelDate: Date,
|
||||||
|
request: express.Request,
|
||||||
|
) {
|
||||||
|
await this.database.doRequest(
|
||||||
|
prisma =>
|
||||||
prisma.member.update({
|
prisma.member.update({
|
||||||
where: { id: memberId },
|
where: { id: memberId },
|
||||||
data: { kuendigungzum: cancelDate },
|
data: { kuendigungzum: cancelDate },
|
||||||
}),
|
}),
|
||||||
|
request,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { inject } from "../infrastructure/di/index.js";
|
import { inject } from "../infrastructure/di/index.js";
|
||||||
import { Claim } from "../models/claim";
|
import { Claim } from "../models/claim.js";
|
||||||
import { Topic } from "../models/topic.js";
|
import { type User } from "../models/user.js";
|
||||||
import { 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 {
|
||||||
@@ -22,7 +21,7 @@ export class UserService {
|
|||||||
return await this.keycloakAdmin.getUsers();
|
return await this.keycloakAdmin.getUsers();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setSubscriptions(token: string, topics: Topic[]) {
|
// public async setSubscriptions(token: string, topics: Topic[]) {
|
||||||
throw new Error("Method not implemented.");
|
// throw new Error("Method not implemented.");
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "^21.0.2",
|
||||||
"@angular/build": "^20.3.8",
|
"@angular/build": "^20.3.8",
|
||||||
"@angular/cli": "^20.3.8",
|
"@angular/cli": "^20.3.8",
|
||||||
"@angular/compiler-cli": "^20.3.0",
|
"@angular/compiler-cli": "^20.3.0",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ApplicationConfig,
|
ApplicationConfig,
|
||||||
|
isDevMode,
|
||||||
provideBrowserGlobalErrorListeners,
|
provideBrowserGlobalErrorListeners,
|
||||||
provideZoneChangeDetection,
|
provideZoneChangeDetection,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
@@ -29,7 +30,7 @@ export const appConfig: ApplicationConfig = {
|
|||||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
provideKeycloak({
|
provideKeycloak({
|
||||||
config: {
|
config: {
|
||||||
url: 'https://keycloak.toni714.de:8443/',
|
url: isDevMode() ? 'http://localhost:8080/' : 'https://keycloak.toni714.de:8443/',
|
||||||
realm: 'taekwondo',
|
realm: 'taekwondo',
|
||||||
clientId: 'angular-frontend',
|
clientId: 'angular-frontend',
|
||||||
},
|
},
|
||||||
@@ -44,7 +45,7 @@ export const appConfig: ApplicationConfig = {
|
|||||||
provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
|
provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
|
||||||
useValue: [
|
useValue: [
|
||||||
createInterceptorCondition<IncludeBearerTokenCondition>({
|
createInterceptorCondition<IncludeBearerTokenCondition>({
|
||||||
urlPattern: /^https:\/\/tkd-api\.toni714\.de.*$/,
|
urlPattern: isDevMode()? /^http:\/\/localhost:3000.*$/ : /^https:\/\/tkd-api\.toni714\.de.*$/,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@if(this.loggedIn()){
|
@if(this.auth.loggedIn()){
|
||||||
<div class="no-print">
|
<div class="no-print">
|
||||||
<button mat-flat-button color="accent" (click)="onLogOut()">LogOut</button>
|
<button mat-flat-button color="accent" (click)="this.auth.logout()">LogOut</button>
|
||||||
<a mat-stroked-button color="primary" [routerLink]="['/home']">Home</a>
|
<button mat-stroked-button color="primary" [routerLink]="['/home']">Home</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const roles = {
|
|||||||
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,
|
||||||
@@ -44,7 +44,12 @@ export const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
loadComponent: () => import('./components/home/home').then((mod) => mod.Home),
|
loadComponent: () => import('./components/home/home').then((mod) => mod.Home),
|
||||||
canActivate: [requireRoles([roles.MemberAdmin, roles.UserAdmin], true)],
|
canActivate: [requireRoles()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'home',
|
||||||
|
loadComponent: () => import('./components/home/home').then((mod) => mod.Home),
|
||||||
|
canActivate: [requireRoles()],
|
||||||
},
|
},
|
||||||
{ path: '**', component: MissingPage },
|
{ path: '**', component: MissingPage },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Component, signal } from '@angular/core';
|
import { Component, inject, signal } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { RouterModule, RouterOutlet } from '@angular/router';
|
import { RouterModule, RouterOutlet } from '@angular/router';
|
||||||
|
import { Authentication } from './services/authentication';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@@ -9,10 +10,6 @@ import { RouterModule, RouterOutlet } from '@angular/router';
|
|||||||
styleUrl: './app.scss',
|
styleUrl: './app.scss',
|
||||||
})
|
})
|
||||||
export class App {
|
export class App {
|
||||||
protected loggedIn = signal(false);
|
|
||||||
protected readonly title = signal('frontend');
|
protected readonly title = signal('frontend');
|
||||||
|
protected readonly auth = inject(Authentication);
|
||||||
protected onLogOut() {
|
|
||||||
this.loggedIn.set(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,14 @@
|
|||||||
<p>home works!</p>
|
<h1>Startseite</h1>
|
||||||
|
<ul>
|
||||||
|
<!--TODO fix theming/colors-->
|
||||||
|
<button matButton="outlined" class="warn-button" [routerLink]="['/test']">
|
||||||
|
Entwichlungsinformationen
|
||||||
|
</button>
|
||||||
|
<button matButton="filled" color="primary" [routerLink]="['/list']">Mitglieder</button>
|
||||||
|
<button matButton="filled" color="warn" [routerLink]="['/manageUsers']">
|
||||||
|
Benutzer Verwalten
|
||||||
|
</button>
|
||||||
|
<button matButton="filled" color="primary" [routerLink]="['/exam']">
|
||||||
|
Prüfungsanmeldung
|
||||||
|
</button>
|
||||||
|
</ul>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-home',
|
selector: 'app-home',
|
||||||
imports: [],
|
imports: [RouterModule, MatButtonModule],
|
||||||
templateUrl: './home.html',
|
templateUrl: './home.html',
|
||||||
styleUrl: './home.scss',
|
styleUrl: './home.scss',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Component, inject } from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
import { Authentication } from '../../services/authentication';
|
import { Authentication } from '../../services/authentication';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -9,7 +10,13 @@ import { Authentication } from '../../services/authentication';
|
|||||||
})
|
})
|
||||||
export class Login {
|
export class Login {
|
||||||
private readonly authentication = inject(Authentication);
|
private readonly authentication = inject(Authentication);
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
|
if(this.authentication.loggedIn()){
|
||||||
|
this.router.navigate(['/home']);
|
||||||
|
}else{
|
||||||
this.authentication.login();
|
this.authentication.login();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { 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,
|
||||||
@@ -22,6 +22,9 @@ export class Authentication {
|
|||||||
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();
|
public readonly authenticationState = this._authenticationState.asReadonly();
|
||||||
|
public readonly loggedIn = computed(()=>{
|
||||||
|
return this.authenticationState() === AuthenticationState.Authenticated;
|
||||||
|
});
|
||||||
public readonly userInfo = this._userInfo.asReadonly();
|
public readonly userInfo = this._userInfo.asReadonly();
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
@@ -69,6 +72,10 @@ export class Authentication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async login(location?: string): Promise<void> {
|
public async login(location?: string): Promise<void> {
|
||||||
|
if(this._authenticationState() === AuthenticationState.Authenticated){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var redirectUri = location
|
var redirectUri = location
|
||||||
? `${window.location.origin}/${location}`
|
? `${window.location.origin}/${location}`
|
||||||
: `${window.location.origin}/login`;
|
: `${window.location.origin}/login`;
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
// This file was generated by running 'ng generate @angular/material:theme-color'.
|
||||||
|
// Proceed with caution if making changes to this file.
|
||||||
|
|
||||||
|
@use 'sass:map';
|
||||||
|
@use '@angular/material' as mat;
|
||||||
|
|
||||||
|
// Note: Color palettes are generated from primary: #0047a0, secondary: #cc9c00, tertiary: #cd2e3a, error: #E63241
|
||||||
|
$_palettes: (
|
||||||
|
primary: (
|
||||||
|
0: #000000,
|
||||||
|
10: #001a43,
|
||||||
|
20: #002d6c,
|
||||||
|
25: #003881,
|
||||||
|
30: #004397,
|
||||||
|
35: #134fa8,
|
||||||
|
40: #275bb5,
|
||||||
|
50: #4575cf,
|
||||||
|
60: #618feb,
|
||||||
|
70: #83aaff,
|
||||||
|
80: #afc6ff,
|
||||||
|
90: #d8e2ff,
|
||||||
|
95: #edf0ff,
|
||||||
|
98: #faf9ff,
|
||||||
|
99: #fefbff,
|
||||||
|
100: #ffffff,
|
||||||
|
),
|
||||||
|
secondary: (
|
||||||
|
0: #000000,
|
||||||
|
10: #251a00,
|
||||||
|
20: #3f2e00,
|
||||||
|
25: #4c3800,
|
||||||
|
30: #5a4300,
|
||||||
|
35: #694f00,
|
||||||
|
40: #775a00,
|
||||||
|
50: #967200,
|
||||||
|
60: #b58a00,
|
||||||
|
70: #d5a411,
|
||||||
|
80: #f3bf33,
|
||||||
|
90: #ffdf99,
|
||||||
|
95: #ffefd2,
|
||||||
|
98: #fff8f2,
|
||||||
|
99: #fffbff,
|
||||||
|
100: #ffffff,
|
||||||
|
),
|
||||||
|
tertiary: (
|
||||||
|
0: #000000,
|
||||||
|
10: #410007,
|
||||||
|
20: #680010,
|
||||||
|
25: #7d0016,
|
||||||
|
30: #92001b,
|
||||||
|
35: #a60a23,
|
||||||
|
40: #b81d2d,
|
||||||
|
50: #db3943,
|
||||||
|
60: #ff535a,
|
||||||
|
70: #ff8887,
|
||||||
|
80: #ffb3b0,
|
||||||
|
90: #ffdad8,
|
||||||
|
95: #ffedeb,
|
||||||
|
98: #fff8f7,
|
||||||
|
99: #fffbff,
|
||||||
|
100: #ffffff,
|
||||||
|
),
|
||||||
|
neutral: (
|
||||||
|
0: #000000,
|
||||||
|
10: #191b21,
|
||||||
|
20: #2e3036,
|
||||||
|
25: #393b42,
|
||||||
|
30: #45464d,
|
||||||
|
35: #515259,
|
||||||
|
40: #5d5e65,
|
||||||
|
50: #75777e,
|
||||||
|
60: #8f9098,
|
||||||
|
70: #aaabb2,
|
||||||
|
80: #c5c6ce,
|
||||||
|
90: #e2e2ea,
|
||||||
|
95: #f0f0f8,
|
||||||
|
98: #faf9ff,
|
||||||
|
99: #fefbff,
|
||||||
|
100: #ffffff,
|
||||||
|
4: #0c0e14,
|
||||||
|
6: #111319,
|
||||||
|
12: #1d1f25,
|
||||||
|
17: #282a30,
|
||||||
|
22: #33353b,
|
||||||
|
24: #37393f,
|
||||||
|
87: #d9d9e1,
|
||||||
|
92: #e7e7f0,
|
||||||
|
94: #ededf5,
|
||||||
|
96: #f3f3fb,
|
||||||
|
),
|
||||||
|
neutral-variant: (
|
||||||
|
0: #000000,
|
||||||
|
10: #171b25,
|
||||||
|
20: #2c303b,
|
||||||
|
25: #373b46,
|
||||||
|
30: #434752,
|
||||||
|
35: #4e525e,
|
||||||
|
40: #5a5e6a,
|
||||||
|
50: #737783,
|
||||||
|
60: #8d909d,
|
||||||
|
70: #a7abb8,
|
||||||
|
80: #c3c6d4,
|
||||||
|
90: #dfe2f0,
|
||||||
|
95: #edf0ff,
|
||||||
|
98: #faf9ff,
|
||||||
|
99: #fefbff,
|
||||||
|
100: #ffffff,
|
||||||
|
),
|
||||||
|
error: (
|
||||||
|
0: #000000,
|
||||||
|
10: #410007,
|
||||||
|
20: #680011,
|
||||||
|
25: #7d0016,
|
||||||
|
30: #92001c,
|
||||||
|
35: #a90021,
|
||||||
|
40: #bd0b29,
|
||||||
|
50: #e12e3e,
|
||||||
|
60: #ff535a,
|
||||||
|
70: #ff8887,
|
||||||
|
80: #ffb3b1,
|
||||||
|
90: #ffdad8,
|
||||||
|
95: #ffedeb,
|
||||||
|
98: #fff8f7,
|
||||||
|
99: #fffbff,
|
||||||
|
100: #ffffff,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$_rest: (
|
||||||
|
secondary: map.get($_palettes, secondary),
|
||||||
|
neutral: map.get($_palettes, neutral),
|
||||||
|
neutral-variant: map.get($_palettes, neutral-variant),
|
||||||
|
error: map.get($_palettes, error),
|
||||||
|
);
|
||||||
|
|
||||||
|
$primary-palette: map.merge(map.get($_palettes, primary), $_rest);
|
||||||
|
$tertiary-palette: map.merge(map.get($_palettes, tertiary), $_rest);
|
||||||
|
|
||||||
|
@function _high-contrast-value($light, $dark, $theme-type) {
|
||||||
|
@if ($theme-type == light) {
|
||||||
|
@return $light;
|
||||||
|
}
|
||||||
|
@if ($theme-type == dark) {
|
||||||
|
@return $dark;
|
||||||
|
}
|
||||||
|
@if ($theme-type == color-scheme) {
|
||||||
|
@return light-dark(#{$light}, #{$dark});
|
||||||
|
}
|
||||||
|
|
||||||
|
@error 'Unknown theme-type #{$theme-type}. Expected light, dark, or color-scheme';
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin high-contrast-overrides($theme-type) {
|
||||||
|
@include mat.theme-overrides((
|
||||||
|
primary: _high-contrast-value(#002963, #ecefff, $theme-type),
|
||||||
|
on-primary: _high-contrast-value(#ffffff, #000000, $theme-type),
|
||||||
|
primary-container: _high-contrast-value(#00459c, #a9c2ff, $theme-type),
|
||||||
|
on-primary-container: _high-contrast-value(#ffffff, #000a23, $theme-type),
|
||||||
|
inverse-primary: _high-contrast-value(#afc6ff, #00449a, $theme-type),
|
||||||
|
primary-fixed: _high-contrast-value(#00459c, #d8e2ff, $theme-type),
|
||||||
|
primary-fixed-dim: _high-contrast-value(#003070, #afc6ff, $theme-type),
|
||||||
|
on-primary-fixed: _high-contrast-value(#ffffff, #000000, $theme-type),
|
||||||
|
on-primary-fixed-variant: _high-contrast-value(#ffffff, #00102f, $theme-type),
|
||||||
|
secondary: _high-contrast-value(#392a00, #ffeecf, $theme-type),
|
||||||
|
on-secondary: _high-contrast-value(#ffffff, #000000, $theme-type),
|
||||||
|
secondary-container: _high-contrast-value(#5d4600, #efbb2f, $theme-type),
|
||||||
|
on-secondary-container: _high-contrast-value(#ffffff, #110a00, $theme-type),
|
||||||
|
secondary-fixed: _high-contrast-value(#5d4600, #ffdf99, $theme-type),
|
||||||
|
secondary-fixed-dim: _high-contrast-value(#413000, #f3bf33, $theme-type),
|
||||||
|
on-secondary-fixed: _high-contrast-value(#ffffff, #000000, $theme-type),
|
||||||
|
on-secondary-fixed-variant: _high-contrast-value(#ffffff, #181000, $theme-type),
|
||||||
|
tertiary: _high-contrast-value(#60000e, #ffecea, $theme-type),
|
||||||
|
on-tertiary: _high-contrast-value(#ffffff, #000000, $theme-type),
|
||||||
|
tertiary-container: _high-contrast-value(#97001c, #ffadab, $theme-type),
|
||||||
|
on-tertiary-container: _high-contrast-value(#ffffff, #220002, $theme-type),
|
||||||
|
tertiary-fixed: _high-contrast-value(#97001c, #ffdad8, $theme-type),
|
||||||
|
tertiary-fixed-dim: _high-contrast-value(#6c0011, #ffb3b0, $theme-type),
|
||||||
|
on-tertiary-fixed: _high-contrast-value(#ffffff, #000000, $theme-type),
|
||||||
|
on-tertiary-fixed-variant: _high-contrast-value(#ffffff, #2d0003, $theme-type),
|
||||||
|
background: _high-contrast-value(#faf9ff, #111319, $theme-type),
|
||||||
|
on-background: _high-contrast-value(#191b21, #e2e2ea, $theme-type),
|
||||||
|
surface: _high-contrast-value(#faf9ff, #111319, $theme-type),
|
||||||
|
surface-dim: _high-contrast-value(#b8b8c0, #111319, $theme-type),
|
||||||
|
surface-bright: _high-contrast-value(#faf9ff, #4e5056, $theme-type),
|
||||||
|
surface-container-low: _high-contrast-value(#f0f0f8, #1d1f25, $theme-type),
|
||||||
|
surface-container-lowest: _high-contrast-value(#ffffff, #000000, $theme-type),
|
||||||
|
surface-container: _high-contrast-value(#e2e2ea, #2e3036, $theme-type),
|
||||||
|
surface-container-high: _high-contrast-value(#d3d4dc, #393b42, $theme-type),
|
||||||
|
surface-container-highest: _high-contrast-value(#c5c6ce, #45464d, $theme-type),
|
||||||
|
on-surface: _high-contrast-value(#000000, #ffffff, $theme-type),
|
||||||
|
shadow: _high-contrast-value(#000000, #000000, $theme-type),
|
||||||
|
scrim: _high-contrast-value(#000000, #000000, $theme-type),
|
||||||
|
surface-tint: _high-contrast-value(#275bb5, #afc6ff, $theme-type),
|
||||||
|
inverse-surface: _high-contrast-value(#2e3036, #e2e2ea, $theme-type),
|
||||||
|
inverse-on-surface: _high-contrast-value(#ffffff, #000000, $theme-type),
|
||||||
|
outline: _high-contrast-value(#282c37, #edeffe, $theme-type),
|
||||||
|
outline-variant: _high-contrast-value(#454954, #bfc2d0, $theme-type),
|
||||||
|
error: _high-contrast-value(#60000e, #ffecea, $theme-type),
|
||||||
|
on-error: _high-contrast-value(#ffffff, #000000, $theme-type),
|
||||||
|
error-container: _high-contrast-value(#97001d, #ffadab, $theme-type),
|
||||||
|
on-error-container: _high-contrast-value(#ffffff, #220002, $theme-type),
|
||||||
|
surface-variant: _high-contrast-value(#dfe2f0, #434752, $theme-type),
|
||||||
|
on-surface-variant: _high-contrast-value(#000000, #ffffff, $theme-type),
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -5,16 +5,24 @@
|
|||||||
// Learn more about theming and how to use it for your application's
|
// Learn more about theming and how to use it for your application's
|
||||||
// custom components at https://material.angular.dev/guide/theming
|
// custom components at https://material.angular.dev/guide/theming
|
||||||
@use '@angular/material' as mat;
|
@use '@angular/material' as mat;
|
||||||
|
@use './styles.colors.scss' as my-theme;
|
||||||
|
|
||||||
|
@include mat.core();
|
||||||
|
|
||||||
|
@include mat.all-component-themes(my-theme.$light-theme);
|
||||||
|
@include mat.color-variants-backwards-compatibility(my-theme.$light-theme);
|
||||||
|
|
||||||
html {
|
html {
|
||||||
@include mat.theme((
|
// @include mat.theme((
|
||||||
color: (
|
// color: (
|
||||||
primary: mat.$cyan-palette,
|
// primary: my-theme.$primary-palette,
|
||||||
tertiary: mat.$orange-palette,
|
// tertiary: my-theme.$tertiary-palette,
|
||||||
),
|
// ),
|
||||||
typography: Roboto,
|
// typography: Roboto,
|
||||||
density: 0,
|
// density: 0,
|
||||||
));
|
// ));
|
||||||
|
@include mat.core-theme(my-theme.$light-theme);
|
||||||
|
@include mat.button-theme(my-theme.$light-theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|||||||
Generated
+5332
-564
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user