dev env setup
This commit is contained in:
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
import { Claim } from "./claim";
|
||||
import { Claim } from "./claim.js";
|
||||
|
||||
export interface User {
|
||||
readonly id?: string;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
// }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user