dev env setup

This commit is contained in:
tw-blue
2025-12-06 21:27:29 +01:00
parent 987a380758
commit bc9f0f9f3b
38 changed files with 5953 additions and 733 deletions
@@ -17,7 +17,9 @@ export class HealthCheckController extends Controller {
@Get("ok")
@SuccessResponse("200", "OK")
public async getOk() {}
public async getOk() {
return "ok";
}
@Get("auth")
@Security(KC_SECURITY_NAME)
@@ -25,6 +27,7 @@ export class HealthCheckController extends Controller {
public async getAuth() {}
@Post("testmessage")
@SuccessResponse("200", "OK")
public async sendTestMessage() {
await this.messaging.sendTestMessage();
}
@@ -6,11 +6,13 @@ import {
Path,
Post,
Put,
Request,
Response,
Route,
Security,
SuccessResponse,
} from "@tsoa/runtime";
import express from "express";
import { inject } from "../infrastructure/di/index.js";
import { ErrorStates } from "../infrastructure/errorStates.js";
import { Claim } from "../models/claim.js";
@@ -22,19 +24,27 @@ import { MembersService } from "../services/membersService.js";
export class MembersContoller extends Controller {
private readonly membersService = inject(MembersService);
@Security(KC_SECURITY_NAME, [Claim.MemberAdmin])
@SuccessResponse("200", "OK")
@Get()
public async getMembers(): Promise<Member[]> {
return await this.membersService.getMembers();
public async getMembers(
@Request() request: express.Request,
): Promise<Member[]> {
return await this.membersService.getMembers(request);
}
@Security(KC_SECURITY_NAME, [Claim.MemberAdmin])
@SuccessResponse("200", "OK")
@Response(404, "Member not found")
@Get("{memberId}")
public async getMember(@Path() memberId: string): Promise<Member> {
public async getMember(
@Path() memberId: string,
@Request() request: express.Request,
): Promise<Member> {
try {
const foundMember =
await this.membersService.getMemberById(memberId);
const foundMember = await this.membersService.getMemberById(
memberId,
request,
);
return foundMember;
} catch (error) {
if (
@@ -53,8 +63,9 @@ export class MembersContoller extends Controller {
@Post()
public async createMember(
@Body() createArgs: MemberCreateArgs,
@Request() request: express.Request,
): Promise<Member> {
return await this.membersService.createMember(createArgs);
return await this.membersService.createMember(createArgs, request);
}
@Security(KC_SECURITY_NAME, [Claim.MemberAdmin])
@@ -62,9 +73,12 @@ export class MembersContoller extends Controller {
@Response(400, "Bad Request")
@Response(404, "Member not found")
@Put()
public async updateMember(@Body() member: Member): Promise<void> {
public async updateMember(
@Body() member: Member,
@Request() request: express.Request,
): Promise<void> {
try {
await this.membersService.updateMember(member);
await this.membersService.updateMember(member, request);
} catch (error) {
if (
error instanceof Error &&
@@ -83,8 +97,13 @@ export class MembersContoller extends Controller {
@Delete("{memberId}")
public async scheduleDeletion(
@Path() memberId: string,
@Body() cancelDate: Date
){
await this.membersService.scheduleDeletion(memberId, cancelDate);
@Body() cancelDate: Date,
@Request() request: express.Request,
) {
await this.membersService.scheduleDeletion(
memberId,
cancelDate,
request,
);
}
}
+15 -14
View File
@@ -1,5 +1,4 @@
import {
Body,
BodyProp,
Controller,
Get,
@@ -8,25 +7,27 @@ import {
Query,
Route,
Security,
SuccessResponse,
} from "@tsoa/runtime";
import { inject } from "../infrastructure/di";
import { Claim } from "../models/claim";
import { Topic } from "../models/topic";
import { User } from "../models/user";
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 { UserService } from "../services/userService";
import { UserService } from "../services/userService.js";
@Route("users")
export class UsersController extends Controller {
private readonly userService = inject(UserService);
@Security(KC_SECURITY_NAME, [Claim.UserAdmin])
@SuccessResponse("200", "OK")
@Get()
public async getUsers(): Promise<User[]> {
return await this.userService.getUsers();
}
@Security(KC_SECURITY_NAME, [Claim.UserAdmin])
@SuccessResponse("200", "OK")
@Put("/claims/{uid}")
public async setClaims(
@Path() uid: string,
@@ -36,12 +37,12 @@ export class UsersController extends Controller {
await this.userService.setClaim(uid, claim, value);
}
@Security(KC_SECURITY_NAME)
@Put("/subscriptions/{token}")
public async setSubscriptions(
@Body() topics: Topic[],
@Path() token: string,
) {
await this.userService.setSubscriptions(token, topics);
}
// @Security(KC_SECURITY_NAME)
// @Put("/subscriptions/{token}")
// public async setSubscriptions(
// @Body() topics: Topic[],
// @Path() token: string,
// ) {
// await this.userService.setSubscriptions(token, topics);
// }
}
+51 -4
View File
@@ -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 { inject } from "./infrastructure/di/injector.js";
import { sessionHandler } from "./infrastructure/sessionHandler.js";
import { EnvironmentService } from "./services/environmentService.js";
import { keycloak } from "./services/keycloak/user/index.js";
const app = express();
const port = 3000;
app.use(sessionHandler);
app.use(keycloak.middleware());
// console.log("not using", keycloak.middleware);
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(
cors({
origin: [
"https://taekwondo-chemnitz.toni714.de",
"https://tcc-1-ev-intern.web.app",
"http://localhost:4200",
"https://localhost:4200",
],
//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);
app.listen(port, () => {
console.log(`Server is running at http://localhost:${port}`);
const environment = inject(EnvironmentService);
// wants to be last
app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => {
res.status(500);
res.send(JSON.stringify(err));
throw err;
});
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";
const INJECTABLE_META = Symbol("injectable");
const INJECTABLE_REGISTRY = new Set<Token>();
export function Injectable(): ClassDecorator {
return function (target: any) {
@@ -10,14 +10,10 @@ export function Injectable(): ClassDecorator {
writable: false,
});
INJECTABLE_REGISTRY.add(target);
currentInjector.add(target as Token);
};
}
export function isInjectable(targert: any) {
return !!targert[INJECTABLE_META];
}
export function getAllInjectables(): Token[] {
return Array.from(INJECTABLE_REGISTRY);
}
+11 -30
View File
@@ -1,21 +1,11 @@
import { getAllInjectables, isInjectable } from "./injectable-decorator.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);
}
import { type Token } from "./types.js";
export class Injector {
private instances = new Map<Token, any>();
private records = new Map<Token, Provider>();
private records = new Set<Token>();
constructor(providers: Provider[]) {
providers.forEach(p => this.records.set(p.provide, p));
add(token: Token) {
this.records.add(token);
}
get<T>(token: Token<T>): T {
@@ -23,8 +13,7 @@ export class Injector {
return this.instances.get(token);
}
const provider = this.records.get(token);
if (!provider) {
if (!this.records.has(token)) {
throw new Error("No provider for token");
}
@@ -38,19 +27,11 @@ export class Injector {
}
}
export function createInjector(injectables: Token[]) {
const providers: Provider[] = injectables.map(injectable => {
if (!isInjectable(injectable)) {
throw new Error(`${injectable.name} is not injectable`);
}
return { provide: injectable, useClass: injectable };
});
export const currentInjector: Injector = new Injector();
return new Injector(providers);
}
export function createGlobalInjector() {
const injectables = getAllInjectables();
return createInjector(injectables);
export function inject<T>(token: Token<T>): T {
if (!currentInjector) {
throw new Error("inject() called outside of injection context");
}
return currentInjector.get(token);
}
@@ -1,4 +1 @@
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[],
response?: Response,
): Promise<any> {
if (roles) {
if (roles && roles.length > 0) {
return await promisifyMiddleware(
request,
keycloak.enforcer(roles?.map(role => `realm:${role}`)),
@@ -35,7 +35,6 @@ export async function expressAuthentication(
roles?: string[],
response?: Response,
): Promise<any> {
console.dir({ request, securityName, roles });
switch (securityName) {
case KC_SECURITY_NAME:
return doKeycloakAuth(request, roles, response);
+1 -1
View File
@@ -1,4 +1,4 @@
import { Claim } from "./claim";
import { Claim } from "./claim.js";
export interface User {
readonly id?: string;
+17 -9
View File
@@ -1,7 +1,13 @@
import { PrismaPg } from "@prisma/adapter-pg";
import { type Request } from "express";
import { PrismaClient } from "../../generated/prisma/client.js";
import { Injectable } from "../../infrastructure/di/injectable-decorator.js";
type ExtendedClient = ReturnType<typeof createExtendedClient>;
type TransactionClient = Omit<
ExtendedClient,
"$connect" | "$disconnect" | "$on" | "$transaction" | "$extends"
>;
export type SafeClient = Omit<
ExtendedClient,
@@ -33,9 +39,9 @@ function createExtendedClient(client: PrismaClient) {
// });
}
@Injectable()
export class DatabaseService {
private readonly prismaClient: ExtendedClient;
private readonly safeClient: SafeClient;
public constructor() {
const dbUser = process.env["TKD_DB_USER"];
@@ -46,17 +52,19 @@ export class DatabaseService {
const adapter = new PrismaPg({ connectionString });
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>(
request: (prisma: SafeClient) => Promise<T>,
command: (prisma: SafeClient) => Promise<T>,
request: Request,
): 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";
import { Injectable } from "../../../infrastructure/di/injectable-decorator.js";
import kcClientConfig from "./nodejs-api.json" with { type: "json" };
import KeycloakAdminClient from "@keycloak/keycloak-admin-client";
import { Claim } from "../../../models/claim";
import { User } from "../../../models/user";
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation.js";
import { Claim } from "../../../models/claim.js";
import { type User } from "../../../models/user.js";
@Injectable()
export class KeycloakAdmin {
@@ -54,7 +54,7 @@ export class KeycloakAdmin {
const client = await this.kcAdminClient;
const kcUsers = await client.users.find();
return kcUsers.map(kcUser => ({
id: kcUser.id,
id: kcUser.id ?? "", //TODO handle undefined id
username: kcUser.username || "",
roles: (kcUser.realmRoles || []).map(role => role as Claim),
}));
@@ -2,16 +2,16 @@
"clientId": "nodejs-api",
"name": "NodejsApi",
"description": "",
"rootUrl": "https://tkd-api.toni714.de",
"adminUrl": "https://tkd-api.toni714.de",
"baseUrl": "https://tkd-api.toni714.de",
"rootUrl": "http://localhost:3000",
"adminUrl": "http://localhost:3000",
"baseUrl": "http://localhost:3000",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"secret": "btdeoaq2mgtoa7SpCEL1phLYcw2gGhGp",
"redirectUris": ["https://tkd-api.toni714.de/*"],
"webOrigins": ["https://tkd-api.toni714.de"],
"secret": "devsecret",
"redirectUris": ["http://localhost:3000/*"],
"webOrigins": ["http://localhost:3000"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
@@ -1,10 +1,10 @@
{
"realm": "taekwondo",
"auth-server-url": "https://keycloak.toni714.de:8443",
"auth-server-url": "https://localhost:8080",
"ssl-required": "external",
"resource": "nodejs-api",
"credentials": {
"secret": "btdeoaq2mgtoa7SpCEL1phLYcw2gGhGp"
"secret": "devsecret"
},
"confidential-port": 0
}
+43 -21
View File
@@ -1,3 +1,4 @@
import express from "express";
import { inject, Injectable } from "../infrastructure/di/index.js";
import { ErrorStates } from "../infrastructure/errorStates.js";
import {
@@ -11,16 +12,22 @@ import { DatabaseService } from "./db/prisma.js";
export class MembersService {
private readonly database = inject(DatabaseService);
public async getMembers(): Promise<Member[]> {
const dbMembers = await this.database.doRequest(prisma =>
prisma.member.findMany(),
public async getMembers(request: express.Request): Promise<Member[]> {
const dbMembers = await this.database.doRequest(
async prisma => await prisma.member.findMany(),
request,
);
return dbMembers.map(mapDbMemberToMember);
}
public async getMemberById(memberId: string): Promise<Member> {
const member = await this.database.doRequest(prisma =>
prisma.member.findFirst({ where: { id: memberId } }),
public async getMemberById(
memberId: string,
request: express.Request,
): Promise<Member> {
const member = await this.database.doRequest(
async prisma =>
await prisma.member.findFirst({ where: { id: memberId } }),
request,
);
if (!member) {
@@ -32,36 +39,51 @@ export class MembersService {
return mapDbMemberToMember(member);
}
public async createMember(createArgs: MemberCreateArgs): Promise<Member> {
const newMember = await this.database.doRequest(prisma =>
prisma.member.create({ data: createArgs }),
public async createMember(
createArgs: MemberCreateArgs,
request: express.Request,
): Promise<Member> {
const newMember = await this.database.doRequest(
async prisma => await prisma.member.create({ data: createArgs }),
request,
);
return mapDbMemberToMember(newMember);
}
public async updateMember(member: Member): Promise<Member> {
public async updateMember(
member: Member,
request: express.Request,
): Promise<Member> {
const id = member.id;
if (!id) {
throw new Error("Member id is required for update", {
cause: ErrorStates.BAD_REQUEST,
});
}
const modifiedMember = await this.database.doRequest(prisma =>
prisma.member.update({
data: { ...member, id: id },
where: { id: id },
}),
const modifiedMember = await this.database.doRequest(
async prisma =>
await prisma.member.update({
data: { ...member, id: id },
where: { id: id },
}),
request,
);
return mapDbMemberToMember(modifiedMember);
}
public async scheduleDeletion(memberId: string, cancelDate: Date) {
await this.database.doRequest(prisma =>
prisma.member.update({
where: { id: memberId },
data: { kuendigungzum: cancelDate },
}),
public async scheduleDeletion(
memberId: string,
cancelDate: Date,
request: express.Request,
) {
await this.database.doRequest(
prisma =>
prisma.member.update({
where: { id: memberId },
data: { kuendigungzum: cancelDate },
}),
request,
);
}
}
+5 -6
View File
@@ -1,7 +1,6 @@
import { inject } from "../infrastructure/di/index.js";
import { Claim } from "../models/claim";
import { Topic } from "../models/topic.js";
import { User } from "../models/user.js";
import { Claim } from "../models/claim.js";
import { type User } from "../models/user.js";
import { KeycloakAdmin } from "./keycloak/admin/index.js";
export class UserService {
@@ -22,7 +21,7 @@ export class UserService {
return await this.keycloakAdmin.getUsers();
}
public async setSubscriptions(token: string, topics: Topic[]) {
throw new Error("Method not implemented.");
}
// public async setSubscriptions(token: string, topics: Topic[]) {
// throw new Error("Method not implemented.");
// }
}