changed to a non nx monorepo

This commit is contained in:
2025-02-14 18:35:39 -06:00
parent a4f77ac63a
commit 2f16c30dad
57 changed files with 20069 additions and 272 deletions

13
api/src/app.module.ts Normal file
View File

@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { DecksController } from './decks.controller';
import { DrizzleService } from './drizzle.service';
import { ProxyController } from './proxy.controller';
import { SqlLoggerService } from './sql-logger.service';
import { UserController } from './user.controller';
@Module({
imports: [],
controllers: [DecksController, ProxyController, UserController],
providers: [DrizzleService, SqlLoggerService],
})
export class AppModule {}

63
api/src/db/schema.ts Normal file
View File

@@ -0,0 +1,63 @@
import * as t from 'drizzle-orm/pg-core';
import { pgEnum, pgTable as table } from 'drizzle-orm/pg-core';
export const rolesEnum = pgEnum('roles', ['admin', 'guest', 'pro']);
export const deck = table(
'deck',
{
id: t.integer('id').primaryKey().generatedAlwaysAsIdentity(),
deckname: t.varchar('deckname').notNull(),
bildname: t.varchar('bildname'),
bildid: t.varchar('bildid'),
x1: t.real('x1'),
x2: t.real('x2'),
y1: t.real('y1'),
y2: t.real('y2'),
due: t.integer('due'),
ivl: t.real('ivl'),
factor: t.real('factor'),
reps: t.integer('reps'),
lapses: t.integer('lapses'),
isGraduated: t.integer('isgraduated'),
user: t.varchar('user').notNull(),
inserted: t.timestamp('inserted', { mode: 'date' }).defaultNow(),
updated: t.timestamp('updated', { mode: 'date' }).defaultNow(),
},
table => [t.uniqueIndex('deck_idx').on(table.id)],
);
export type InsertDeck = typeof deck.$inferInsert;
export type SelectDeck = typeof deck.$inferSelect;
export const users = table(
'users',
{
id: t.integer().primaryKey().generatedAlwaysAsIdentity(),
name: t.varchar('name', { length: 256 }),
email: t.varchar().notNull(),
role: rolesEnum().default('guest'),
sign_in_provider: t.varchar('sign_in_provider', { length: 50 }),
lastLogin: t.timestamp('lastLogin', { mode: 'date' }).defaultNow(),
numberOfLogins: t.integer('numberOfLogins').default(1), // Neue Spalte
},
table => [t.uniqueIndex('users_idx').on(table.id)],
);
export type InsertUser = typeof users.$inferInsert;
export type SelectUser = typeof users.$inferSelect;
export interface User {
name: string;
picture: string;
iss: string;
aud: string;
auth_time: number;
user_id: string;
sub: string;
iat: number;
exp: number;
email: string;
email_verified: boolean;
firebase: {
identities: any;
sign_in_provider: string;
};
uid: string;
}

204
api/src/decks.controller.ts Normal file
View File

@@ -0,0 +1,204 @@
// decks.controller.ts
import {
Body,
Controller,
Delete,
Get,
HttpException,
HttpStatus,
Param,
Post,
Put,
Request,
UseGuards,
} from '@nestjs/common';
import { User } from './db/schema';
import { DrizzleService } from './drizzle.service';
import { AuthGuard } from './service/auth.guard';
@Controller('decks')
@UseGuards(AuthGuard)
export class DecksController {
constructor(private readonly drizzleService: DrizzleService) {}
@Get()
async getDecks(@Request() req) {
const user: User = req['user'];
const entries = await this.drizzleService.getDecks(user);
const decks = {};
for (const entry of entries) {
const deckname = entry.deckname!!;
if (!decks[deckname]) {
decks[deckname] = {
name: deckname,
images: [],
};
}
if (entry.bildname && entry.bildid) {
decks[deckname].images.push({
name: entry.bildname,
bildid: entry.bildid,
id: entry.id,
x1: entry.x1,
x2: entry.x2,
y1: entry.y1,
y2: entry.y2,
due: entry.due,
ivl: entry.ivl,
factor: entry.factor,
reps: entry.reps,
lapses: entry.lapses,
isGraduated: Boolean(entry.isGraduated),
inserted: new Date(entry.inserted!!),
updated: new Date(entry.updated!!),
});
}
}
return Object.values(decks);
}
@Post()
async createDeck(@Request() req, @Body() data: { deckname: string }) {
if (!data.deckname) {
throw new HttpException('No deckname provided', HttpStatus.BAD_REQUEST);
}
const user: User = req['user'];
return this.drizzleService.createDeck(data.deckname, user);
}
@Get(':deckname')
async getDeck(@Request() req, @Param('deckname') deckname: string) {
const user: User = req['user'];
const entries = await this.drizzleService.getDeckByName(deckname, user);
if (entries.length === 0) {
throw new HttpException('Deck not found', HttpStatus.NOT_FOUND);
}
const deck = {
name: deckname,
images: [] as any,
};
for (const entry of entries) {
if (entry.bildname && entry.bildid) {
deck.images.push({
name: entry.bildname,
bildid: entry.bildid,
id: entry.id,
x1: entry.x1,
x2: entry.x2,
y1: entry.y1,
y2: entry.y2,
due: entry.due,
ivl: entry.ivl,
factor: entry.factor,
reps: entry.reps,
lapses: entry.lapses,
isGraduated: Boolean(entry.isGraduated),
inserted: new Date(entry.inserted!!),
updated: new Date(entry.updated!!),
});
}
}
return deck;
}
@Delete(':deckname')
async deleteDeck(@Request() req, @Param('deckname') deckname: string) {
const user: User = req['user'];
return this.drizzleService.deleteDeck(deckname, user);
}
@Put(':oldDeckname/rename')
async renameDeck(
@Request() req,
@Param('oldDeckname') oldDeckname: string,
@Body() data: { newDeckName: string },
) {
if (!data.newDeckName) {
throw new HttpException(
'New deck name is required',
HttpStatus.BAD_REQUEST,
);
}
const user: User = req['user'];
return this.drizzleService.renameDeck(oldDeckname, data.newDeckName, user);
}
@Post('image')
async updateImage(@Request() req, @Body() data: any) {
if (!data) {
throw new HttpException('No data provided', HttpStatus.BAD_REQUEST);
}
const user: User = req['user'];
const requiredFields = ['deckname', 'bildname', 'bildid', 'boxes'];
if (!requiredFields.every((field) => field in data)) {
throw new HttpException('Missing fields in data', HttpStatus.BAD_REQUEST);
}
if (!Array.isArray(data.boxes) || data.boxes.length === 0) {
throw new HttpException(
"'boxes' must be a non-empty list",
HttpStatus.BAD_REQUEST,
);
}
return this.drizzleService.updateImage(data, user);
}
@Put('image/:bildid/rename')
async renameImage(
@Request() req,
@Param('bildid') bildid: string,
@Body() data: { newImageName: string },
) {
if (!data.newImageName) {
throw new HttpException(
'New image name is required',
HttpStatus.BAD_REQUEST,
);
}
const user: User = req['user'];
return this.drizzleService.renameImage(bildid, data.newImageName, user);
}
@Delete('image/:bildid')
async deleteImagesByBildId(@Request() req, @Param('bildid') bildid: string) {
const user: User = req['user'];
return this.drizzleService.deleteImagesByBildId(bildid, user);
}
@Post('images/:bildid/move')
async moveImage(
@Request() req,
@Param('bildid') bildid: string,
@Body() data: { targetDeckId: string },
) {
if (!data.targetDeckId) {
throw new HttpException(
'No targetDeckId provided',
HttpStatus.BAD_REQUEST,
);
}
const user: User = req['user'];
return this.drizzleService.moveImage(bildid, data.targetDeckId, user);
}
@Put('boxes/:id')
async updateBox(
@Request() req,
@Param('id') id: number,
@Body()
data: {
due?: number;
ivl?: number;
factor?: number;
reps?: number;
lapses?: number;
isGraduated?: boolean;
},
) {
const user: User = req['user'];
return this.drizzleService.updateBox(id, data, user);
}
}

505
api/src/drizzle.service.ts Normal file
View File

@@ -0,0 +1,505 @@
// drizzle.service.ts
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { and, eq, sql } from 'drizzle-orm';
import { drizzle } from 'drizzle-orm/node-postgres';
import { deck, InsertUser, SelectDeck, User, users } from './db/schema';
import { SqlLoggerService } from './sql-logger.service';
@Injectable()
export class DrizzleService {
// private readonly logger = new Logger(DrizzleService.name);
private db: any;
constructor(private sqlLogger: SqlLoggerService) {
this.db = drizzle(process.env['DATABASE_URL']!, {
logger: {
logQuery: (query: string, params: any[]) => {
this.sqlLogger.logQuery(query, params);
},
},
});
}
/**
* Hilfsmethode: Ermittelt die Rolle eines Users anhand der E-Mail.
*/
private async getUserRole(email: string): Promise<'guest' | 'pro' | 'admin'> {
const result = await this.db
.select({ role: users.role })
.from(users)
.where(eq(users.email, email))
.limit(1);
if (result.length === 0) {
// Falls der User nicht gefunden wird, gehen wir von "guest" aus.
return 'guest';
}
return result[0].role;
}
/**
* Methode zum Abrufen der Decks eines Benutzers.
* Hier wird unterschieden in Deck-Header (deck.bildid IS NULL) und
* zugehörige Bilder (deck.bildid IS NOT NULL). Zudem werden
* - für "guest" maximal 2 Decks und 20 Bilder pro Deck
* - für "pro" maximal 10 Decks und 40 Bilder pro Deck
* zurückgegeben.
*/
async getDecks(user: User) {
const role = await this.getUserRole(user.email);
const maxDecks =
role === 'guest' ? 2 : role === 'pro' ? 10 : Number.MAX_SAFE_INTEGER;
const maxImages =
role === 'guest' ? 20 : role === 'pro' ? 40 : Number.MAX_SAFE_INTEGER;
// Abrufen der Deck-Header (d.h. Einträge ohne Bilddaten)
const deckHeaders = await this.db
.select()
.from(deck)
.where(and(eq(deck.user, user.email), sql`bildid IS NULL`))
.limit(maxDecks);
// Für jedes Deck werden nun die zugehörigen Bilder (Einträge mit bildid) abgerufen.
const imagesForDecks = await Promise.all(
deckHeaders.map(async (header: any) => {
const images: Array<any> = await this.db
.select()
.from(deck)
.where(
and(
eq(deck.user, user.email),
eq(deck.deckname, header.deckname),
sql`bildid IS NOT NULL`,
),
);
const minimizedImagesArray = (() => {
const selectedBildnamen: Array<any> = [];
// Sammle die ersten maxImages verschiedenen Bildnamen
for (const item of images) {
if (
!selectedBildnamen.includes(item.bildname) &&
selectedBildnamen.length < maxImages
) {
selectedBildnamen.push(item.bildname);
}
}
// Filtere das Array, um nur Einträge mit diesen Bildnamen zu behalten
return images.filter((item) =>
selectedBildnamen.includes(item.bildname),
);
})();
return { minimizedImagesArray };
//return { images };
}),
);
return imagesForDecks.flatMap((item) => item.minimizedImagesArray);
}
/**
* Methode zum Erstellen eines neuen Decks.
* Hier wird geprüft, ob der User (basierend auf seiner Rolle) bereits
* die maximale Anzahl an Decks erstellt hat.
*/
async createDeck(deckname: string, user: User) {
const role = await this.getUserRole(user.email);
const maxDecks =
role === 'guest' ? 2 : role === 'pro' ? 10 : Number.MAX_SAFE_INTEGER;
// Zähle nur die Deck-Header (Einträge ohne Bilddaten)
const deckCountResult = await this.db
.select({ count: sql`count(*) as count` })
.from(deck)
.where(and(eq(deck.user, user.email), sql`bildid IS NULL`))
.all();
const deckCount = Number(deckCountResult[0].count);
if (deckCount >= maxDecks) {
throw new HttpException(
`Maximale Anzahl an Decks (${maxDecks}) erreicht für Rolle "${role}"`,
HttpStatus.FORBIDDEN,
);
}
// 'inserted' und 'updated' werden automatisch von der Datenbank gesetzt
const result = await this.db
.insert(deck)
.values({
deckname,
user: user.email,
})
.returning();
return { status: 'success', deck: result };
}
/**
* Methode zum Abrufen eines Decks nach Name.
* (Hinweis: Diese Methode wird intern z.B. in updateImage verwendet.
* Daher wird hier nicht die Limitierung angewendet.)
*/
async getDeckByName(deckname: string, user: User) {
return this.db
.select()
.from(deck)
.where(and(eq(deck.deckname, deckname), eq(deck.user, user.email)));
}
/**
* Methode zum Löschen eines Decks.
*/
async deleteDeck(deckname: string, user: User) {
const existingDeck = await this.getDeckByName(deckname, user);
if (existingDeck.length === 0) {
throw new HttpException('Deck not found', HttpStatus.NOT_FOUND);
}
await this.db
.delete(deck)
.where(and(eq(deck.deckname, deckname), eq(deck.user, user.email)));
return { status: 'success' };
}
/**
* Methode zum Umbenennen eines Decks.
*/
async renameDeck(oldDeckname: string, newDeckname: string, user: User) {
const existingDeck = await this.getDeckByName(oldDeckname, user);
if (existingDeck.length === 0) {
throw new HttpException('Deck not found', HttpStatus.NOT_FOUND);
}
const existingNewDeck = await this.getDeckByName(newDeckname, user);
if (existingNewDeck.length > 0) {
throw new HttpException(
'Deck with the new name already exists',
HttpStatus.CONFLICT,
);
}
await this.db
.update(deck)
.set({
deckname: newDeckname,
updated: new Date(),
})
.where(and(eq(deck.deckname, oldDeckname), eq(deck.user, user.email)));
return {
status: 'success',
message: 'Deck renamed successfully',
};
}
/**
* Methode zum Aktualisieren eines Bildes innerhalb eines Decks.
* Hier wird vor dem Anlegen eines neuen Bildes geprüft, ob das
* maximale Limit pro Deck (basierend auf der User-Rolle) erreicht wurde.
*/
async updateImage(
data: {
deckname: string;
bildname: string;
bildid: string;
boxes: Array<{
x1: number;
x2: number;
y1: number;
y2: number;
id?: number;
}>;
},
user: User,
) {
// Schritt 1: Überprüfen, ob das Deck existiert
const existingDecks: SelectDeck[] = await this.getDeckByName(
data.deckname,
user,
);
if (existingDecks.length === 0) {
throw new HttpException('Deck not found', HttpStatus.NOT_FOUND);
}
// Rollenbasierte Limitierung: max. Bilder pro Deck
const role = await this.getUserRole(user.email);
const maxImages =
role === 'guest' ? 20 : role === 'pro' ? 40 : Number.MAX_SAFE_INTEGER;
// Prüfen, ob für die angegebene bildid bereits Einträge existieren.
const imageGroupExists = existingDecks.some(
(row) => row.bildid === data.bildid,
);
if (!imageGroupExists) {
// Zähle die unterschiedlichen Bilder (distinct bildid, wobei nur Einträge mit gesetzter bildid zählen)
const distinctImagesResult = await this.db
.select({ count: sql`count(distinct bildid) as count` })
.from(deck)
.where(
and(
eq(deck.user, user.email),
eq(deck.deckname, data.deckname),
sql`bildid IS NOT NULL`,
),
)
.all();
const distinctImageCount = Number(distinctImagesResult[0].count);
if (distinctImageCount >= maxImages) {
throw new HttpException(
`Maximale Anzahl an Bildern (${maxImages}) für Deck "${data.deckname}" erreicht`,
HttpStatus.FORBIDDEN,
);
}
}
// Schritt 2: Trennen der neuen und bestehenden Einträge
const newEntries = data.boxes.filter((b) => !b.id);
const existingEntries = data.boxes.filter((b) => b.id);
// Schritt 3: Einfügen neuer Einträge
const insertedImages: any = [];
for (let index = 0; index < newEntries.length; index++) {
const box = newEntries[index];
const result = await this.db
.insert(deck)
.values({
deckname: data.deckname,
bildname: data.bildname,
bildid: data.bildid,
x1: box.x1,
x2: box.x2,
y1: box.y1,
y2: box.y2,
user: user.email,
})
.returning();
insertedImages.push(result);
}
// Schritt 4: Aktualisieren bestehender Einträge
for (let index = 0; index < existingEntries.length; index++) {
const box = existingEntries[index];
const existingDeck = existingDecks.find((d) => d.id === box.id);
if (
existingDeck &&
(existingDeck.x1 !== box.x1 ||
existingDeck.x2 !== box.x2 ||
existingDeck.y1 !== box.y1 ||
existingDeck.y2 !== box.y2)
) {
const result = await this.db
.update(deck)
.set({
x1: box.x1,
x2: box.x2,
y1: box.y1,
y2: box.y2,
updated: new Date(),
})
.where(
and(
eq(deck.user, user.email),
eq(deck.bildid, data.bildid),
eq(deck.deckname, data.deckname),
eq(deck.id, box.id!),
),
);
if (result.rowsAffected === 0) {
throw new HttpException(
`Box with id ${box.id} not found`,
HttpStatus.NOT_FOUND,
);
}
}
}
// Schritt 5: Löschen von nicht mehr vorhandenen Einträgen
const existingIdsInDb = existingDecks
.filter(
(entry) =>
entry.bildid === data.bildid && entry.deckname === data.deckname,
)
.map((entry) => entry.id);
const incomingIds = existingEntries.map((entry) => entry.id);
const idsToDelete = existingIdsInDb.filter(
(id) => !incomingIds.includes(id),
);
if (idsToDelete.length > 0) {
await this.db
.delete(deck)
.where(
and(
eq(deck.deckname, data.deckname),
eq(deck.bildid, data.bildid),
eq(deck.user, user.email),
sql`id in ${idsToDelete}`,
),
);
}
return { status: 'success', inserted_images: insertedImages };
}
/**
* Methode zum Löschen von Bildern anhand der bildid.
*/
async deleteImagesByBildId(bildid: string, user: User) {
const affectedDecks = await this.db
.select({ deckname: deck.deckname })
.from(deck)
.where(and(eq(deck.bildid, bildid), eq(deck.user, user.email)));
if (affectedDecks.length === 0) {
throw new HttpException(
'No entries found for the given image ID',
HttpStatus.NOT_FOUND,
);
}
await this.db
.delete(deck)
.where(and(eq(deck.bildid, bildid), eq(deck.user, user.email)));
return {
status: 'success',
message: `All entries for image ID "${bildid}" have been deleted.`,
};
}
/**
* Methode zum Verschieben eines Bildes in ein anderes Deck.
*/
async moveImage(bildid: string, targetDeckId: string, user: User) {
const existingImages = await this.db
.select()
.from(deck)
.where(and(eq(deck.bildid, bildid), eq(deck.user, user.email)));
if (existingImages.length === 0) {
throw new HttpException(
'No entries found for the given image ID',
HttpStatus.NOT_FOUND,
);
}
await this.db
.update(deck)
.set({
deckname: targetDeckId,
updated: new Date(),
})
.where(and(eq(deck.bildid, bildid), eq(deck.user, user.email)));
return { status: 'success', moved_entries: existingImages.length };
}
/**
* Methode zum Umbenennen eines Bildes.
*/
async renameImage(bildId: string, newImagename: string, user: User) {
const existingImages = await this.db
.select()
.from(deck)
.where(and(eq(deck.bildid, bildId), eq(deck.user, user.email)))
.all();
if (existingImages.length === 0) {
throw new HttpException('Deck not found', HttpStatus.NOT_FOUND);
}
await this.db
.update(deck)
.set({
bildname: newImagename,
updated: new Date(),
})
.where(and(eq(deck.bildid, bildId), eq(deck.user, user.email)));
return {
status: 'success',
message: 'Image Entries renamed successfully',
};
}
/**
* Methode zum Aktualisieren einer Box.
*/
async updateBox(
id: number,
data: {
due?: number;
ivl?: number;
factor?: number;
reps?: number;
lapses?: number;
isGraduated?: boolean;
inserted?: string;
},
user: User,
) {
const updateData: any = { ...data };
if (typeof data.isGraduated === 'boolean') {
updateData.isGraduated = Number(data.isGraduated);
}
updateData.updated = new Date();
updateData.inserted = new Date(data.inserted as string);
const result = await this.db
.update(deck)
.set(updateData)
.where(and(eq(deck.id, id), eq(deck.user, user.email)));
if (result.rowsAffected === 0) {
throw new HttpException('Box not found', HttpStatus.NOT_FOUND);
}
return { status: 'success' };
}
/**
* Methode zum Abrufen aller eindeutigen Bild-IDs aus der Datenbank.
*/
async getDistinctBildIds(user: User): Promise<string[]> {
try {
const result = await this.db
.selectDistinct([deck.bildid])
.from(deck)
.all();
const usedIds = result
.map((row: any) => row['0'])
.filter((id: string | null) => id !== null) as string[];
console.log(usedIds);
return usedIds;
} catch (error) {
this.sqlLogger.logQuery('Error fetching distinct bildids', []);
throw new HttpException(
`Fehler beim Abrufen der Bild-IDs - ${error}`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/**
* Führt den Login-Vorgang durch:
* - Existiert der Benutzer bereits (überprüft via E-Mail), wird das Feld `lastLogin`
* (und ggf. weitere Felder) aktualisiert.
* - Existiert der Benutzer nicht, wird ein neuer Datensatz angelegt.
*/
async logIn(createUserDto: InsertUser) {
const existingUser = await this.db
.select()
.from(users)
.where(eq(users.email, createUserDto.email))
.limit(1);
if (existingUser.length > 0) {
const updatedUser = await this.db
.update(users)
.set({
lastLogin: new Date(),
name: createUserDto.name,
sign_in_provider: createUserDto.sign_in_provider,
numberOfLogins: sql`${users.numberOfLogins} + 1`,
})
.where(eq(users.email, createUserDto.email))
.returning();
return updatedUser;
} else {
const insertedUser = await this.db
.insert(users)
.values({
name: createUserDto.name,
email: createUserDto.email,
sign_in_provider: createUserDto.sign_in_provider,
lastLogin: new Date(),
role: 'guest',
})
.returning();
return insertedUser;
}
}
}

21
api/src/main.ts Normal file
View File

@@ -0,0 +1,21 @@
import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { json, urlencoded } from 'express';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: ['log', 'error', 'warn', 'debug', 'verbose'], // Aktiviere alle Log-Level
});
app.use(json({ limit: '50mb' }));
app.use(urlencoded({ limit: '50mb', extended: true }));
const globalPrefix = 'api';
app.setGlobalPrefix(globalPrefix);
const port = process.env['PORT'] || 3000;
await app.listen(port);
Logger.log(
`🚀 Application is running on: http://localhost:${port}/${globalPrefix}`,
);
}
void bootstrap();

133
api/src/proxy.controller.ts Normal file
View File

@@ -0,0 +1,133 @@
// decks.controller.ts
import {
Body,
Controller,
HttpException,
HttpStatus,
Post,
Res,
UseGuards,
} from '@nestjs/common';
import express from 'express';
import { DrizzleService } from './drizzle.service';
import { AuthGuard } from './service/auth.guard';
@Controller('')
@UseGuards(AuthGuard)
export class ProxyController {
constructor(private readonly drizzleService: DrizzleService) {}
// --------------------
// Proxy Endpoints
// --------------------
@Post('ocr')
async ocrEndpoint(
@Body() data: { image: string },
@Res() res: express.Response,
) {
try {
if (!data || !data.image) {
throw new HttpException('No image provided', HttpStatus.BAD_REQUEST);
}
const response = await fetch('http://localhost:5000/api/ocr', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ image: data.image }),
});
const result = await response.json();
if (!response.ok) {
if (response.status === 400) {
throw new HttpException(result.error, HttpStatus.BAD_REQUEST);
}
throw new HttpException(
result.error || 'OCR processing failed',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
// Bei erfolgreicher Verarbeitung mit Warnung
if (result.warning) {
return res.status(HttpStatus.OK).json({
warning: result.warning,
debug_dir: result.debug_dir,
});
}
// Bei vollständig erfolgreicher Verarbeitung
return res.status(HttpStatus.OK).json({
status: result.status,
results: result.results,
});
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
'Internal server error',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
// --------------------
// Cleanup Endpoint
// --------------------
@Post('cleanup')
async cleanupEndpoint(
@Body() data: { dryrun?: boolean },
@Res() res: express.Response,
) {
try {
const user = res.req['user']; // Benutzerinformationen aus dem Request
// 2. Nur Benutzer mit der spezifischen E-Mail dürfen fortfahren
if (user.email !== 'andreas.knuth@gmail.com') {
throw new HttpException('Zugriff verweigert.', HttpStatus.FORBIDDEN);
}
// 1. Abrufen der distinct bildid aus der Datenbank
const usedIds = await this.drizzleService.getDistinctBildIds(user);
// 3. Verarbeitung des dryrun Parameters
const dryrun = data.dryrun !== undefined ? data.dryrun : true;
if (typeof dryrun !== 'boolean') {
throw new HttpException(
"'dryrun' muss ein boolescher Wert sein.",
HttpStatus.BAD_REQUEST,
);
}
// 4. Aufruf des Flask-Backend-Endpunkts
const response = await fetch('http://localhost:5000/api/cleanup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ dryrun, usedIds }),
});
const result = await response.json();
if (!response.ok) {
throw new HttpException(
result.error || 'Cleanup failed',
response.status,
);
}
// 5. Rückgabe der Ergebnisse an den Client
return res.status(HttpStatus.OK).json(result);
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
'Interner Serverfehler',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@@ -0,0 +1,27 @@
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import admin from './firebase-admin';
@Injectable()
export class AuthGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('No token provided');
}
try {
const decodedToken = await admin.auth().verifyIdToken(token);
request['user'] = decodedToken; // Fügen Sie die Benutzerdaten dem Request-Objekt hinzu
return true;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers['authorization']?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}

View File

@@ -0,0 +1,16 @@
import * as admin from 'firebase-admin';
import { ServiceAccount } from 'firebase-admin';
const serviceAccount: ServiceAccount = {
projectId: process.env['FIREBASE_PROJECT_ID'],
clientEmail: process.env['FIREBASE_CLIENT_EMAIL'],
privateKey: process.env['FIREBASE_PRIVATE_KEY']?.replace(/\\n/g, '\n'), // Ersetzen Sie escaped newlines
};
if (!admin.apps.length) {
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
}
export default admin;

View File

@@ -0,0 +1,36 @@
// sql-logger.service.ts
import { Injectable, Logger } from '@nestjs/common';
import type { ChalkInstance } from 'chalk';
import chalk from 'chalk';
@Injectable()
export class SqlLoggerService {
private readonly logger = new Logger(SqlLoggerService.name);
private chalkInstance: ChalkInstance;
constructor() {
this.chalkInstance = chalk;
}
logQuery(query: string, params: any[], sourceIp?: string) {
const timestamp = new Date().toISOString().replace('T', ' ').replace('Z', '');
let logMessage = `[DrizzleORM] Info\t${timestamp}`;
// Optional source IP
if (sourceIp) {
logMessage += ` IP: ${sourceIp}`;
}
// Colored query and params using chalk directly
const coloredQuery = chalk.blueBright(`Query: ${query}`);
const coloredParams = chalk.yellow(`Params: ${JSON.stringify(params)}`);
logMessage += ` - ${coloredQuery} - ${coloredParams}`;
// Add timing indicator
logMessage += chalk.gray(' +2ms');
console.log(logMessage);
}
}

View File

@@ -0,0 +1,17 @@
// user.controller.ts
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import type { InsertUser } from './db/schema';
import { DrizzleService } from './drizzle.service';
import { AuthGuard } from './service/auth.guard';
@Controller('users')
@UseGuards(AuthGuard)
export class UserController {
constructor(private readonly drizzleService: DrizzleService) {}
@Post()
async createUser(@Body() createUserDto: InsertUser) {
// Hier kannst du zusätzliche Validierungen oder Logik einbauen.
return await this.drizzleService.logIn(createUserDto);
}
}