Authentication with firebase, Landing Page 1.Teil
This commit is contained in:
parent
d17578d123
commit
83035a6b82
5
api/.env
5
api/.env
|
|
@ -1,2 +1,5 @@
|
||||||
DB_FILE_NAME=file:local.db
|
DB_FILE_NAME=file:local.db
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
FIREBASE_PROJECT_ID=haiki-452bd
|
||||||
|
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-fbsvc@haiki-452bd.iam.gserviceaccount.com
|
||||||
|
FIREBASE_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDyCsRhtPYwozBy\n60A4LguqsFzJG0WwCMhvi7PIxoh1kVenwxXBBQHvgssF/jPTkqbK6orL9r15gdRc\nZK2S73OdESYlO9xCE+gq/pop1F432DrHBA6ftQIl2NHSQfkKgTKFM6Pt85e6s5mb\neIxxHr7AzGMlqu7UJbnw8Vk2+8LP3NZXwsyCEi5vg4+8UgPFVxhLnB4HYZIM8JOj\nM3q1N/KqKgl2qGl5ekZLN7QLPs6+znlVZVIeHo1vuX9xTUaa6XUA0xeZfFO3pNUd\nKLlQNAy7OFQ6GlTFTWWHnShecLPHLQdWP15rkwBPv1q7qDmtLMy4E2XLvsoErhJM\niFsbEczxAgMBAAECggEAJqGJTn7vfDvPk8fwbAcNXaTgakisCricJRGLFFR7mygj\ncWc1paUC9hNODBrScsZJUMG2fW9YNnh+SHDZM0Z8kWkXSYIQWYuL1rDkMiDvGMKu\nPu1q2BqvyRKeCoz1DrQoOBJR67yhTu8zaRkIcVWS5Hq6qFxr2fhbgRVEQ/5SzZIG\nPh+Npxdp62Xe66MB0OzmF/A5qSrXTpOOk4/Lmpoqf6PtmrOD+SetE+Aa6ELYX3pK\n30XPLiUyDS+EFPjHLA1U7frOawLRpMP6Iobu7hUzu9ASzgLKxpzbcGRsPtbVRuAQ\nzP+iV+Nyn/wMFbnnjrjlwk/jqE+NLGJnf7Jrc9vwQQKBgQD5lfRWR6MKDezty5Qu\nPGrSlKXOM+aOoTmZiaPTutYzwWHeqzfUfYsghbgR3jl7I0BTqMaGrOnYU60IDWZi\nJeK6iu68pUe8Mme3vXm15Go55UhZD9U5/4W+x0/+AVivBPUBwKUXFdgmPFkb2jBY\np1LJmXaelx2jvPM3yMXQN+5flQKBgQD4Qy6aF3V7DIVxo/F88KCGNUpEobyXsxv6\nakSV+7WEtuSf6amXHxZyiJPtjCIwzCUL458gUd6mwiQgWRy7awdKreU6BCHgqNVf\nAE5ahjeeiLOc1uu0ocNLL+g35DbBXSgSlB+hUE0+bxAxU9xjNc9UZKqIi/kGRP/G\nlxi2ZUIQ7QKBgQDUH5Ku0evL29IGuQOT2F2h5ByXiJznlDd0Ovs2NJFhI3ae3T5y\nJtFcLsomxYxtD6TYdZVlWQjWhyeEtH7T5AczLGmDg6XYWa61ByCuaxetZSV8LGy5\nAmcVoihmZZaOCdSCTM0DNdmjhZ7mgSad8nf2R6v9VconI6xDOSyGr0K1kQKBgQCp\nuhxxIpqhzlSo9aFSfpvwRRyKQVzTBZOaJu7O7zARFIzHOxNDivBoyzD/FXAGlnq5\nXxvaF761mULjjqjTBQAOMUbm3A5hLmv5sBbhUqNR0jmhf1nTu0ft7km/dFlu5wZP\ndU8OlPzKM1oJr0Cb3xzooI3qHm/YtnF7Tq+Jez6onQKBgQCfbqG5dyTAduhPZzmp\n6m4ndzzdYVoh8KiM3fUApo/u9zF3GixUFgcKKFzP1/zmD6A6UfbVT+JVmUfIVtor\nAA42lqJyOaNH9ttQjZeDXfPpbAyAoZzH0l7/U9KSOkh+2I8tMscjWFqyQ1/DGNcY\nptIRECjQrk5jL0yea0+tbpMmJQ==\n-----END PRIVATE KEY-----\n
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,18 @@
|
||||||
// decks.controller.ts
|
// decks.controller.ts
|
||||||
import express from 'express';
|
import { Body, Controller, Delete, Get, HttpException, HttpStatus, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
|
||||||
import {
|
import { User } from '../db/schema';
|
||||||
Controller,
|
import { AuthGuard } from '../service/auth.guard';
|
||||||
Get,
|
|
||||||
Post,
|
|
||||||
Delete,
|
|
||||||
Body,
|
|
||||||
Param,
|
|
||||||
Query,
|
|
||||||
Put,
|
|
||||||
HttpException,
|
|
||||||
HttpStatus,
|
|
||||||
Res,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { DrizzleService } from './drizzle.service';
|
import { DrizzleService } from './drizzle.service';
|
||||||
import { firstValueFrom } from 'rxjs';
|
|
||||||
|
|
||||||
@Controller('decks')
|
@Controller('decks')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
export class DecksController {
|
export class DecksController {
|
||||||
constructor(private readonly drizzleService: DrizzleService) {}
|
constructor(private readonly drizzleService: DrizzleService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async getDecks() {
|
async getDecks(@Request() req) {
|
||||||
const entries = await this.drizzleService.getDecks();
|
const user: User = req['user'];
|
||||||
|
const entries = await this.drizzleService.getDecks(user);
|
||||||
const decks = {};
|
const decks = {};
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const deckname = entry.deckname!!;
|
const deckname = entry.deckname!!;
|
||||||
|
|
@ -55,16 +45,18 @@ export class DecksController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
async createDeck(@Body() data: { deckname: string }) {
|
async createDeck(@Request() req, @Body() data: { deckname: string }) {
|
||||||
if (!data.deckname) {
|
if (!data.deckname) {
|
||||||
throw new HttpException('No deckname provided', HttpStatus.BAD_REQUEST);
|
throw new HttpException('No deckname provided', HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
return this.drizzleService.createDeck(data.deckname);
|
const user: User = req['user'];
|
||||||
|
return this.drizzleService.createDeck(data.deckname, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':deckname')
|
@Get(':deckname')
|
||||||
async getDeck(@Param('deckname') deckname: string) {
|
async getDeck(@Request() req, @Param('deckname') deckname: string) {
|
||||||
const entries = await this.drizzleService.getDeckByName(deckname);
|
const user: User = req['user'];
|
||||||
|
const entries = await this.drizzleService.getDeckByName(deckname, user);
|
||||||
if (entries.length === 0) {
|
if (entries.length === 0) {
|
||||||
throw new HttpException('Deck not found', HttpStatus.NOT_FOUND);
|
throw new HttpException('Deck not found', HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
@ -98,40 +90,36 @@ export class DecksController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':deckname')
|
@Delete(':deckname')
|
||||||
async deleteDeck(@Param('deckname') deckname: string) {
|
async deleteDeck(@Request() req, @Param('deckname') deckname: string) {
|
||||||
return this.drizzleService.deleteDeck(deckname);
|
const user: User = req['user'];
|
||||||
|
return this.drizzleService.deleteDeck(deckname, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put(':oldDeckname/rename')
|
@Put(':oldDeckname/rename')
|
||||||
async renameDeck(
|
async renameDeck(@Request() req, @Param('oldDeckname') oldDeckname: string, @Body() data: { newDeckName: string }) {
|
||||||
@Param('oldDeckname') oldDeckname: string,
|
|
||||||
@Body() data: { newDeckName: string },
|
|
||||||
) {
|
|
||||||
if (!data.newDeckName) {
|
if (!data.newDeckName) {
|
||||||
throw new HttpException('New deck name is required', HttpStatus.BAD_REQUEST);
|
throw new HttpException('New deck name is required', HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
return this.drizzleService.renameDeck(oldDeckname, data.newDeckName);
|
const user: User = req['user'];
|
||||||
|
return this.drizzleService.renameDeck(oldDeckname, data.newDeckName, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('image')
|
@Post('image')
|
||||||
async updateImage(@Body() data: any) {
|
async updateImage(@Request() req, @Body() data: any) {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
throw new HttpException('No data provided', HttpStatus.BAD_REQUEST);
|
throw new HttpException('No data provided', HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
const user: User = req['user'];
|
||||||
const requiredFields = ['deckname', 'bildname', 'bildid', 'boxes'];
|
const requiredFields = ['deckname', 'bildname', 'bildid', 'boxes'];
|
||||||
if (!requiredFields.every((field) => field in data)) {
|
if (!requiredFields.every(field => field in data)) {
|
||||||
throw new HttpException('Missing fields in data', HttpStatus.BAD_REQUEST);
|
throw new HttpException('Missing fields in data', HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(data.boxes) || data.boxes.length === 0) {
|
if (!Array.isArray(data.boxes) || data.boxes.length === 0) {
|
||||||
throw new HttpException(
|
throw new HttpException("'boxes' must be a non-empty list", HttpStatus.BAD_REQUEST);
|
||||||
"'boxes' must be a non-empty list",
|
|
||||||
HttpStatus.BAD_REQUEST,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.drizzleService.updateImage(data);
|
return this.drizzleService.updateImage(data, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete('image/:bildid')
|
@Delete('image/:bildid')
|
||||||
|
|
@ -140,10 +128,7 @@ export class DecksController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('images/:bildid/move')
|
@Post('images/:bildid/move')
|
||||||
async moveImage(
|
async moveImage(@Param('bildid') bildid: string, @Body() data: { targetDeckId: string }) {
|
||||||
@Param('bildid') bildid: string,
|
|
||||||
@Body() data: { targetDeckId: string },
|
|
||||||
) {
|
|
||||||
if (!data.targetDeckId) {
|
if (!data.targetDeckId) {
|
||||||
throw new HttpException('No targetDeckId provided', HttpStatus.BAD_REQUEST);
|
throw new HttpException('No targetDeckId provided', HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
@ -165,6 +150,4 @@ export class DecksController {
|
||||||
) {
|
) {
|
||||||
return this.drizzleService.updateBox(boxId, data);
|
return this.drizzleService.updateBox(boxId, data);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,75 +1,63 @@
|
||||||
// drizzle.service.ts
|
// drizzle.service.ts
|
||||||
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||||
|
import { and, eq, isNull } from 'drizzle-orm';
|
||||||
import { drizzle } from 'drizzle-orm/libsql';
|
import { drizzle } from 'drizzle-orm/libsql';
|
||||||
import { Deck, InsertDeck } from '../db/schema';
|
import { Deck, User } from '../db/schema';
|
||||||
import { eq, and, isNull } from 'drizzle-orm';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DrizzleService {
|
export class DrizzleService {
|
||||||
private db = drizzle('file:local.db');
|
private db = drizzle('file:local.db');
|
||||||
|
|
||||||
async getDecks() {
|
async getDecks(user: User) {
|
||||||
return this.db.select().from(Deck).all();
|
return this.db.select().from(Deck).where(eq(Deck.user, user.email));
|
||||||
}
|
}
|
||||||
|
|
||||||
async createDeck(deckname: string) {
|
async createDeck(deckname: string, user: User) {
|
||||||
const result = await this.db.insert(Deck).values({ deckname }).returning();
|
const result = await this.db.insert(Deck).values({ deckname, user: user.email }).returning();
|
||||||
return { status: 'success', deck: result };
|
return { status: 'success', deck: result };
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDeckByName(deckname: string) {
|
async getDeckByName(deckname: string, user: User) {
|
||||||
return this.db.select().from(Deck).where(eq(Deck.deckname, deckname)).all();
|
return this.db
|
||||||
|
.select()
|
||||||
|
.from(Deck)
|
||||||
|
.where(and(eq(Deck.deckname, deckname), eq(Deck.user, user.email)));
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteDeck(deckname: string) {
|
async deleteDeck(deckname: string, user: User) {
|
||||||
const existingDeck = await this.getDeckByName(deckname);
|
const existingDeck = await this.getDeckByName(deckname, user);
|
||||||
if (existingDeck.length === 0) {
|
if (existingDeck.length === 0) {
|
||||||
throw new HttpException('Deck not found', HttpStatus.NOT_FOUND);
|
throw new HttpException('Deck not found', HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.db.delete(Deck).where(eq(Deck.deckname, deckname));
|
await this.db.delete(Deck).where(and(eq(Deck.deckname, deckname), eq(Deck.user, user.email)));
|
||||||
return { status: 'success' };
|
return { status: 'success' };
|
||||||
}
|
}
|
||||||
|
|
||||||
async renameDeck(oldDeckname: string, newDeckname: string) {
|
async renameDeck(oldDeckname: string, newDeckname: string, user: User) {
|
||||||
const existingDeck = await this.getDeckByName(oldDeckname);
|
const existingDeck = await this.getDeckByName(oldDeckname, user);
|
||||||
if (existingDeck.length === 0) {
|
if (existingDeck.length === 0) {
|
||||||
throw new HttpException('Deck not found', HttpStatus.NOT_FOUND);
|
throw new HttpException('Deck not found', HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingNewDeck = await this.getDeckByName(newDeckname);
|
const existingNewDeck = await this.getDeckByName(newDeckname, user);
|
||||||
if (existingNewDeck.length > 0) {
|
if (existingNewDeck.length > 0) {
|
||||||
throw new HttpException(
|
throw new HttpException('Deck with the new name already exists', HttpStatus.CONFLICT);
|
||||||
'Deck with the new name already exists',
|
|
||||||
HttpStatus.CONFLICT,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.db
|
await this.db.update(Deck).set({ deckname: newDeckname }).where(eq(Deck.deckname, oldDeckname));
|
||||||
.update(Deck)
|
|
||||||
.set({ deckname: newDeckname })
|
|
||||||
.where(eq(Deck.deckname, oldDeckname))
|
|
||||||
return { status: 'success', message: 'Deck renamed successfully' };
|
return { status: 'success', message: 'Deck renamed successfully' };
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateImage(data: {
|
async updateImage(data: { deckname: string; bildname: string; bildid: string; boxes: Array<{ x1: number; x2: number; y1: number; y2: number }> }, user: User) {
|
||||||
deckname: string;
|
const existingDeck = await this.getDeckByName(data.deckname, user);
|
||||||
bildname: string;
|
|
||||||
bildid: string;
|
|
||||||
boxes: Array<{ x1: number; x2: number; y1: number; y2: number }>;
|
|
||||||
}) {
|
|
||||||
const existingDeck = await this.getDeckByName(data.deckname);
|
|
||||||
if (existingDeck.length === 0) {
|
if (existingDeck.length === 0) {
|
||||||
throw new HttpException('Deck not found', HttpStatus.NOT_FOUND);
|
throw new HttpException('Deck not found', HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.db
|
await this.db.delete(Deck).where(and(eq(Deck.deckname, data.deckname), eq(Deck.bildid, data.bildid)));
|
||||||
.delete(Deck)
|
|
||||||
.where(
|
|
||||||
and(eq(Deck.deckname, data.deckname), eq(Deck.bildid, data.bildid)),
|
|
||||||
)
|
|
||||||
|
|
||||||
const insertedImages:any = [];
|
const insertedImages: any = [];
|
||||||
for (let index = 0; index < data.boxes.length; index++) {
|
for (let index = 0; index < data.boxes.length; index++) {
|
||||||
const box = data.boxes[index];
|
const box = data.boxes[index];
|
||||||
const result = await this.db
|
const result = await this.db
|
||||||
|
|
@ -83,7 +71,9 @@ export class DrizzleService {
|
||||||
x2: box.x2,
|
x2: box.x2,
|
||||||
y1: box.y1,
|
y1: box.y1,
|
||||||
y2: box.y2,
|
y2: box.y2,
|
||||||
}).returning();
|
user: 'andreas.knuth@gmail.com',
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
insertedImages.push(result);
|
insertedImages.push(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,17 +81,10 @@ export class DrizzleService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteImagesByBildId(bildid: string) {
|
async deleteImagesByBildId(bildid: string) {
|
||||||
const affectedDecks = await this.db
|
const affectedDecks = await this.db.select({ deckname: Deck.deckname }).from(Deck).where(eq(Deck.bildid, bildid)).all();
|
||||||
.select({ deckname: Deck.deckname })
|
|
||||||
.from(Deck)
|
|
||||||
.where(eq(Deck.bildid, bildid))
|
|
||||||
.all();
|
|
||||||
|
|
||||||
if (affectedDecks.length === 0) {
|
if (affectedDecks.length === 0) {
|
||||||
throw new HttpException(
|
throw new HttpException('No entries found for the given image ID', HttpStatus.NOT_FOUND);
|
||||||
'No entries found for the given image ID',
|
|
||||||
HttpStatus.NOT_FOUND,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.db.delete(Deck).where(eq(Deck.bildid, bildid));
|
await this.db.delete(Deck).where(eq(Deck.bildid, bildid));
|
||||||
|
|
@ -110,22 +93,18 @@ export class DrizzleService {
|
||||||
const remainingImages = await this.db
|
const remainingImages = await this.db
|
||||||
.select()
|
.select()
|
||||||
.from(Deck)
|
.from(Deck)
|
||||||
.where(
|
.where(and(eq(Deck.deckname, deck.deckname), eq(Deck.bildid, bildid)))
|
||||||
and(eq(Deck.deckname, deck.deckname), eq(Deck.bildid, bildid)),
|
|
||||||
)
|
|
||||||
.all();
|
.all();
|
||||||
|
|
||||||
if (remainingImages.length === 0) {
|
if (remainingImages.length === 0) {
|
||||||
const emptyDeckEntry = await this.db
|
const emptyDeckEntry = await this.db
|
||||||
.select()
|
.select()
|
||||||
.from(Deck)
|
.from(Deck)
|
||||||
.where(
|
.where(and(eq(Deck.deckname, deck.deckname), isNull(Deck.bildid)))
|
||||||
and(eq(Deck.deckname, deck.deckname), isNull(Deck.bildid)),
|
|
||||||
)
|
|
||||||
.all();
|
.all();
|
||||||
|
|
||||||
if (emptyDeckEntry.length === 0) {
|
if (emptyDeckEntry.length === 0) {
|
||||||
await this.db.insert(Deck).values({ deckname: deck.deckname });
|
await this.db.insert(Deck).values({ deckname: deck.deckname, user: 'andreas.knuth@gmail.com' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -137,23 +116,13 @@ export class DrizzleService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async moveImage(bildid: string, targetDeckId: string) {
|
async moveImage(bildid: string, targetDeckId: string) {
|
||||||
const existingImages = await this.db
|
const existingImages = await this.db.select().from(Deck).where(eq(Deck.bildid, bildid)).all();
|
||||||
.select()
|
|
||||||
.from(Deck)
|
|
||||||
.where(eq(Deck.bildid, bildid))
|
|
||||||
.all();
|
|
||||||
|
|
||||||
if (existingImages.length === 0) {
|
if (existingImages.length === 0) {
|
||||||
throw new HttpException(
|
throw new HttpException('No entries found for the given image ID', HttpStatus.NOT_FOUND);
|
||||||
'No entries found for the given image ID',
|
|
||||||
HttpStatus.NOT_FOUND,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.db
|
await this.db.update(Deck).set({ deckname: targetDeckId }).where(eq(Deck.bildid, bildid));
|
||||||
.update(Deck)
|
|
||||||
.set({ deckname: targetDeckId })
|
|
||||||
.where(eq(Deck.bildid, bildid))
|
|
||||||
|
|
||||||
return { status: 'success', moved_entries: existingImages.length };
|
return { status: 'success', moved_entries: existingImages.length };
|
||||||
}
|
}
|
||||||
|
|
@ -174,10 +143,7 @@ export class DrizzleService {
|
||||||
updateData.isGraduated = Number(data.isGraduated);
|
updateData.isGraduated = Number(data.isGraduated);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.db
|
const result = await this.db.update(Deck).set(updateData).where(eq(Deck.id, boxId));
|
||||||
.update(Deck)
|
|
||||||
.set(updateData)
|
|
||||||
.where(eq(Deck.id, boxId))
|
|
||||||
|
|
||||||
if (result.rowsAffected === 0) {
|
if (result.rowsAffected === 0) {
|
||||||
throw new HttpException('Box not found', HttpStatus.NOT_FOUND);
|
throw new HttpException('Box not found', HttpStatus.NOT_FOUND);
|
||||||
|
|
@ -185,4 +151,4 @@ export class DrizzleService {
|
||||||
|
|
||||||
return { status: 'success' };
|
return { status: 'success' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,79 +1,64 @@
|
||||||
// decks.controller.ts
|
// decks.controller.ts
|
||||||
|
import { Body, Controller, HttpException, HttpStatus, Post, Res, UseGuards } from '@nestjs/common';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import {
|
import { AuthGuard } from '../service/auth.guard';
|
||||||
Controller,
|
|
||||||
Get,
|
|
||||||
Post,
|
|
||||||
Delete,
|
|
||||||
Body,
|
|
||||||
Param,
|
|
||||||
Query,
|
|
||||||
Put,
|
|
||||||
HttpException,
|
|
||||||
HttpStatus,
|
|
||||||
Res,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { DrizzleService } from './drizzle.service';
|
|
||||||
import { firstValueFrom } from 'rxjs';
|
|
||||||
|
|
||||||
@Controller('')
|
@Controller('')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
export class ProxyController {
|
export class ProxyController {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
// --------------------
|
// --------------------
|
||||||
// Proxy Endpoints
|
// Proxy Endpoints
|
||||||
// --------------------
|
// --------------------
|
||||||
// @Get('debug_image/:name/:filename')
|
// @Get('debug_image/:name/:filename')
|
||||||
// async getDebugImage(
|
// async getDebugImage(
|
||||||
// @Param('name') name: string,
|
// @Param('name') name: string,
|
||||||
// @Param('filename') filename: string,
|
// @Param('filename') filename: string,
|
||||||
// @Res() res: express.Response,
|
// @Res() res: express.Response,
|
||||||
// ) {
|
// ) {
|
||||||
// const url = `http://localhost:8080/api/debug_image/${name}/${filename}`;
|
// const url = `http://localhost:8080/api/debug_image/${name}/${filename}`;
|
||||||
// //const url = `http://localhost:5000/api/debug_image/20250112_112306_9286e3bf/thumbnail.jpg`;
|
// //const url = `http://localhost:5000/api/debug_image/20250112_112306_9286e3bf/thumbnail.jpg`;
|
||||||
|
|
||||||
// try {
|
// try {
|
||||||
// // Fetch the image from the external service
|
// // Fetch the image from the external service
|
||||||
// const response = await fetch(url);
|
// const response = await fetch(url);
|
||||||
|
|
||||||
// // Check if the response is OK (status code 200-299)
|
// // Check if the response is OK (status code 200-299)
|
||||||
// if (!response.ok) {
|
// if (!response.ok) {
|
||||||
// throw new Error(`Failed to retrieve image: ${response.statusText}`);
|
// throw new Error(`Failed to retrieve image: ${response.statusText}`);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// // Get the image data as a buffer
|
// // Get the image data as a buffer
|
||||||
// const imageBuffer = await response.arrayBuffer();
|
// const imageBuffer = await response.arrayBuffer();
|
||||||
|
|
||||||
// // Determine the Content-Type based on the file extension
|
// // Determine the Content-Type based on the file extension
|
||||||
// let contentType = 'image/png'; // Default MIME type
|
// let contentType = 'image/png'; // Default MIME type
|
||||||
// if (filename.toLowerCase().endsWith('.jpg') || filename.toLowerCase().endsWith('.jpeg')) {
|
// if (filename.toLowerCase().endsWith('.jpg') || filename.toLowerCase().endsWith('.jpeg')) {
|
||||||
// contentType = 'image/jpeg';
|
// contentType = 'image/jpeg';
|
||||||
// } else if (filename.toLowerCase().endsWith('.gif')) {
|
// } else if (filename.toLowerCase().endsWith('.gif')) {
|
||||||
// contentType = 'image/gif';
|
// contentType = 'image/gif';
|
||||||
// } else if (filename.toLowerCase().endsWith('.bmp')) {
|
// } else if (filename.toLowerCase().endsWith('.bmp')) {
|
||||||
// contentType = 'image/bmp';
|
// contentType = 'image/bmp';
|
||||||
// } else if (filename.toLowerCase().endsWith('.tiff') || filename.toLowerCase().endsWith('.tif')) {
|
// } else if (filename.toLowerCase().endsWith('.tiff') || filename.toLowerCase().endsWith('.tif')) {
|
||||||
// contentType = 'image/tiff';
|
// contentType = 'image/tiff';
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// // Set the Content-Type header and send the image data
|
// // Set the Content-Type header and send the image data
|
||||||
// res.set('Content-Type', contentType);
|
// res.set('Content-Type', contentType);
|
||||||
// res.send(Buffer.from(imageBuffer));
|
// res.send(Buffer.from(imageBuffer));
|
||||||
// } catch (error) {
|
// } catch (error) {
|
||||||
// // Handle errors
|
// // Handle errors
|
||||||
// res.status(500).json({ error: error.message });
|
// res.status(500).json({ error: error.message });
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
@Post('ocr')
|
@Post('ocr')
|
||||||
async ocrEndpoint(
|
async ocrEndpoint(@Body() data: { image: string }, @Res() res: express.Response) {
|
||||||
@Body() data: { image: string },
|
|
||||||
@Res() res: express.Response,
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
if (!data || !data.image) {
|
if (!data || !data.image) {
|
||||||
throw new HttpException('No image provided', HttpStatus.BAD_REQUEST);
|
throw new HttpException('No image provided', HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('http://localhost:5000/api/ocr', {
|
const response = await fetch('http://localhost:5000/api/ocr', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -81,19 +66,16 @@ export class ProxyController {
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ image: data.image }),
|
body: JSON.stringify({ image: data.image }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 400) {
|
if (response.status === 400) {
|
||||||
throw new HttpException(result.error, HttpStatus.BAD_REQUEST);
|
throw new HttpException(result.error, HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
throw new HttpException(
|
throw new HttpException(result.error || 'OCR processing failed', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
result.error || 'OCR processing failed',
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bei erfolgreicher Verarbeitung mit Warnung
|
// Bei erfolgreicher Verarbeitung mit Warnung
|
||||||
if (result.warning) {
|
if (result.warning) {
|
||||||
return res.status(HttpStatus.OK).json({
|
return res.status(HttpStatus.OK).json({
|
||||||
|
|
@ -101,21 +83,17 @@ export class ProxyController {
|
||||||
debug_dir: result.debug_dir,
|
debug_dir: result.debug_dir,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bei vollständig erfolgreicher Verarbeitung
|
// Bei vollständig erfolgreicher Verarbeitung
|
||||||
return res.status(HttpStatus.OK).json({
|
return res.status(HttpStatus.OK).json({
|
||||||
status: result.status,
|
status: result.status,
|
||||||
results: result.results,
|
results: result.results,
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpException) {
|
if (error instanceof HttpException) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
throw new HttpException(
|
throw new HttpException('Internal server error', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
'Internal server error',
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,50 @@
|
||||||
import { sqliteTable as table, text, integer, real } from 'drizzle-orm/sqlite-core';
|
import * as t from 'drizzle-orm/sqlite-core';
|
||||||
import * as t from "drizzle-orm/sqlite-core";
|
import { integer, real, sqliteTable as table, text } from 'drizzle-orm/sqlite-core';
|
||||||
|
|
||||||
export const Deck = table('Deck', {
|
export const Deck = table(
|
||||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
'Deck',
|
||||||
deckname: text('deckname').notNull(),
|
{
|
||||||
bildname: text('bildname'),
|
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||||
bildid: text('bildid'),
|
deckname: text('deckname').notNull(),
|
||||||
iconindex: integer('iconindex'),
|
bildname: text('bildname'),
|
||||||
x1: real('x1'),
|
bildid: text('bildid'),
|
||||||
x2: real('x2'),
|
iconindex: integer('iconindex'),
|
||||||
y1: real('y1'),
|
x1: real('x1'),
|
||||||
y2: real('y2'),
|
x2: real('x2'),
|
||||||
due: integer('due'),
|
y1: real('y1'),
|
||||||
ivl: real('ivl'),
|
y2: real('y2'),
|
||||||
factor: real('factor'),
|
due: integer('due'),
|
||||||
reps: integer('reps'),
|
ivl: real('ivl'),
|
||||||
lapses: integer('lapses'),
|
factor: real('factor'),
|
||||||
isGraduated: integer('isGraduated'),
|
reps: integer('reps'),
|
||||||
},
|
lapses: integer('lapses'),
|
||||||
(table) => {
|
isGraduated: integer('isGraduated'),
|
||||||
return {
|
user: text('user').notNull(),
|
||||||
index: t.uniqueIndex("email_idx").on(table.id),
|
},
|
||||||
};
|
table => {
|
||||||
}
|
return {
|
||||||
|
index: t.uniqueIndex('email_idx').on(table.id),
|
||||||
|
};
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export type InsertDeck = typeof Deck.$inferInsert;
|
export type InsertDeck = typeof Deck.$inferInsert;
|
||||||
export type SelectDeck = typeof Deck.$inferSelect;
|
export type SelectDeck = typeof Deck.$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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
CREATE TABLE `Deck` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`deckname` text NOT NULL,
|
||||||
|
`bildname` text,
|
||||||
|
`bildid` text,
|
||||||
|
`iconindex` integer,
|
||||||
|
`x1` real,
|
||||||
|
`x2` real,
|
||||||
|
`y1` real,
|
||||||
|
`y2` real,
|
||||||
|
`due` integer,
|
||||||
|
`ivl` real,
|
||||||
|
`factor` real,
|
||||||
|
`reps` integer,
|
||||||
|
`lapses` integer,
|
||||||
|
`isGraduated` integer
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `email_idx` ON `Deck` (`id`);
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE `Deck` ADD `user` text NOT NULL;
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "3996df1c-a197-4842-869e-2e3b4f10064a",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"tables": {
|
||||||
|
"Deck": {
|
||||||
|
"name": "Deck",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"deckname": {
|
||||||
|
"name": "deckname",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"bildname": {
|
||||||
|
"name": "bildname",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"bildid": {
|
||||||
|
"name": "bildid",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"iconindex": {
|
||||||
|
"name": "iconindex",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"x1": {
|
||||||
|
"name": "x1",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"x2": {
|
||||||
|
"name": "x2",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"y1": {
|
||||||
|
"name": "y1",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"y2": {
|
||||||
|
"name": "y2",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"due": {
|
||||||
|
"name": "due",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"ivl": {
|
||||||
|
"name": "ivl",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"factor": {
|
||||||
|
"name": "factor",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"reps": {
|
||||||
|
"name": "reps",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"lapses": {
|
||||||
|
"name": "lapses",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"isGraduated": {
|
||||||
|
"name": "isGraduated",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"email_idx": {
|
||||||
|
"name": "email_idx",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "a3cf5e86-4f1b-4cdc-9688-cf9063ba6936",
|
||||||
|
"prevId": "3996df1c-a197-4842-869e-2e3b4f10064a",
|
||||||
|
"tables": {
|
||||||
|
"Deck": {
|
||||||
|
"name": "Deck",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"deckname": {
|
||||||
|
"name": "deckname",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"bildname": {
|
||||||
|
"name": "bildname",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"bildid": {
|
||||||
|
"name": "bildid",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"iconindex": {
|
||||||
|
"name": "iconindex",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"x1": {
|
||||||
|
"name": "x1",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"x2": {
|
||||||
|
"name": "x2",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"y1": {
|
||||||
|
"name": "y1",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"y2": {
|
||||||
|
"name": "y2",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"due": {
|
||||||
|
"name": "due",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"ivl": {
|
||||||
|
"name": "ivl",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"factor": {
|
||||||
|
"name": "factor",
|
||||||
|
"type": "real",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"reps": {
|
||||||
|
"name": "reps",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"lapses": {
|
||||||
|
"name": "lapses",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"isGraduated": {
|
||||||
|
"name": "isGraduated",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"name": "user",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"email_idx": {
|
||||||
|
"name": "email_idx",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1737229773082,
|
||||||
|
"tag": "0000_ambitious_hercules",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1737229845910,
|
||||||
|
"tag": "0001_smooth_iron_lad",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"migrations": [
|
||||||
|
{
|
||||||
|
"version": "19.0.0",
|
||||||
|
"factory": "./use-application-builder/migration",
|
||||||
|
"description": "Migrate application projects to the new build system. Application projects that are using the '@angular-devkit/build-angular' package's 'browser' and/or 'browser-esbuild' builders will be migrated to use the new 'application' builder. You can read more about this, including known issues and limitations, here: https://angular.dev/tools/cli/build-system-migration",
|
||||||
|
"optional": true,
|
||||||
|
"recommended": true,
|
||||||
|
"documentation": "tools/cli/build-system-migration",
|
||||||
|
"package": "@angular/cli",
|
||||||
|
"name": "use-application-builder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "19.0.0",
|
||||||
|
"factory": "./update-workspace-config/migration",
|
||||||
|
"description": "Update the workspace configuration by replacing deprecated options in 'angular.json' for compatibility with the latest Angular CLI changes.",
|
||||||
|
"package": "@angular/cli",
|
||||||
|
"name": "update-workspace-config"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "19.0.0",
|
||||||
|
"factory": "./update-ssr-imports/migration",
|
||||||
|
"description": "Update '@angular/ssr' import paths to use the new '/node' entry point when 'CommonEngine' is detected.",
|
||||||
|
"package": "@angular/cli",
|
||||||
|
"name": "update-ssr-imports"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
49
package.json
49
package.json
|
|
@ -10,14 +10,15 @@
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^18.2.0",
|
"@angular/animations": "19.1.1",
|
||||||
"@angular/common": "^18.2.0",
|
"@angular/common": "19.1.1",
|
||||||
"@angular/compiler": "^18.2.0",
|
"@angular/compiler": "19.1.1",
|
||||||
"@angular/core": "^18.2.0",
|
"@angular/core": "19.1.1",
|
||||||
"@angular/forms": "^18.2.0",
|
"@angular/fire": "^19.0.0",
|
||||||
"@angular/platform-browser": "^18.2.0",
|
"@angular/forms": "19.1.1",
|
||||||
"@angular/platform-browser-dynamic": "^18.2.0",
|
"@angular/platform-browser": "19.1.1",
|
||||||
"@angular/router": "^18.2.0",
|
"@angular/platform-browser-dynamic": "19.1.1",
|
||||||
|
"@angular/router": "19.1.1",
|
||||||
"@libsql/client": "^0.14.0",
|
"@libsql/client": "^0.14.0",
|
||||||
"@nestjs/common": "^10.0.2",
|
"@nestjs/common": "^10.0.2",
|
||||||
"@nestjs/core": "^10.0.2",
|
"@nestjs/core": "^10.0.2",
|
||||||
|
|
@ -25,29 +26,31 @@
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"fabric": "^5.4.1",
|
"fabric": "^5.4.1",
|
||||||
|
"firebase": "^11.2.0",
|
||||||
|
"firebase-admin": "^13.0.2",
|
||||||
"flowbite": "^2.5.2",
|
"flowbite": "^2.5.2",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.14.10"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^18.2.12",
|
"@angular-devkit/core": "19.1.2",
|
||||||
"@angular-devkit/core": "^18.2.12",
|
"@angular-devkit/schematics": "19.1.2",
|
||||||
"@angular-devkit/schematics": "^18.2.12",
|
"@angular/build": "^19.1.2",
|
||||||
"@angular/cli": "^18.2.12",
|
"@angular/cli": "19.1.2",
|
||||||
"@angular/compiler-cli": "^18.2.0",
|
"@angular/compiler-cli": "19.1.1",
|
||||||
"@nestjs/schematics": "^10.0.1",
|
"@nestjs/schematics": "^10.0.1",
|
||||||
"@nestjs/testing": "^10.0.2",
|
"@nestjs/testing": "^10.0.2",
|
||||||
"@nx/angular": "20.3.1",
|
"@nx/angular": "20.3.2",
|
||||||
"@nx/js": "20.3.1",
|
"@nx/js": "20.3.2",
|
||||||
"@nx/nest": "^20.3.1",
|
"@nx/nest": "20.3.2",
|
||||||
"@nx/node": "20.3.1",
|
"@nx/node": "20.3.2",
|
||||||
"@nx/web": "20.3.1",
|
"@nx/web": "20.3.2",
|
||||||
"@nx/webpack": "20.3.1",
|
"@nx/webpack": "20.3.2",
|
||||||
"@nx/workspace": "20.3.1",
|
"@nx/workspace": "20.3.2",
|
||||||
"@schematics/angular": "^18.2.12",
|
"@schematics/angular": "^19.1.2",
|
||||||
"@swc-node/register": "~1.9.1",
|
"@swc-node/register": "~1.9.1",
|
||||||
"@swc/core": "~1.5.7",
|
"@swc/core": "~1.5.7",
|
||||||
"@swc/helpers": "~0.5.11",
|
"@swc/helpers": "~0.5.11",
|
||||||
|
|
@ -56,7 +59,7 @@
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.1.2",
|
||||||
"drizzle-kit": "^0.30.2",
|
"drizzle-kit": "^0.30.2",
|
||||||
"drizzle-orm": "^0.38.4",
|
"drizzle-orm": "^0.38.4",
|
||||||
"nx": "20.3.1",
|
"nx": "20.3.2",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.6.2",
|
||||||
"tailwindcss": "^3.4.15",
|
"tailwindcss": "^3.4.15",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
|
|
|
||||||
14
project.json
14
project.json
|
|
@ -35,7 +35,7 @@
|
||||||
"prefix": "app",
|
"prefix": "app",
|
||||||
"targets": {
|
"targets": {
|
||||||
"build": {
|
"build": {
|
||||||
"executor": "@angular-devkit/build-angular:application",
|
"executor": "@angular/build:application",
|
||||||
"options": {
|
"options": {
|
||||||
"outputPath": "dist/vokabeltraining",
|
"outputPath": "dist/vokabeltraining",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
|
|
@ -54,7 +54,13 @@
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss"
|
"src/styles.scss"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": [],
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/app/environments/environment.ts",
|
||||||
|
"with": "src/app/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
|
|
@ -81,7 +87,7 @@
|
||||||
"defaultConfiguration": "production"
|
"defaultConfiguration": "production"
|
||||||
},
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"executor": "@angular-devkit/build-angular:dev-server",
|
"executor": "@angular/build:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"proxyConfig": "proxy.conf.json"
|
"proxyConfig": "proxy.conf.json"
|
||||||
},
|
},
|
||||||
|
|
@ -96,7 +102,7 @@
|
||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"executor": "@angular-devkit/build-angular:extract-i18n"
|
"executor": "@angular/build:extract-i18n"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component } from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { Auth } from '@angular/fire/auth';
|
||||||
|
import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
|
||||||
import { DeckListComponent } from './deck-list.component';
|
import { DeckListComponent } from './deck-list.component';
|
||||||
|
import { ClickOutsideDirective } from './service/click-outside.directive';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
template: `
|
template: `
|
||||||
|
|
@ -25,22 +27,101 @@ import { DeckListComponent } from './deck-list.component';
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="isLoggedIn" class="container mx-auto p-4">
|
<div *ngIf="isLoggedIn" class="container mx-auto p-4">
|
||||||
<h1 class="text-3xl font-bold text-center mb-8">Vocabulary Training</h1>
|
<div class="flex justify-center items-center mb-8">
|
||||||
|
<h1 class="text-3xl font-bold mx-auto">Vocabulary Training</h1>
|
||||||
|
<div class="relative" appClickOutside (clickOutside)="showDropdown = false">
|
||||||
|
<img [src]="photoURL" alt="User Photo" class="w-10 h-10 rounded-full cursor-pointer" (click)="toggleDropdown()" />
|
||||||
|
<div *ngIf="showDropdown" class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg">
|
||||||
|
<button (click)="logout()" class="block w-full text-left px-4 py-2 text-gray-700 hover:bg-gray-100">Abmelden</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<app-deck-list></app-deck-list>
|
<app-deck-list></app-deck-list>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, DeckListComponent],
|
styles: `
|
||||||
|
img {
|
||||||
|
border: 2px solid #fff;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
img:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stile für das Dropdown-Menü */
|
||||||
|
.dropdown {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown button {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown button:hover {
|
||||||
|
background-color: #f7fafc;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
imports: [CommonModule, DeckListComponent, ClickOutsideDirective],
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
isLoggedIn = false; // Zustand für den Login-Status
|
isLoggedIn = false;
|
||||||
|
private auth: Auth = inject(Auth);
|
||||||
|
showDropdown = false;
|
||||||
|
photoURL: string = 'https://via.placeholder.com/40';
|
||||||
|
// user: User | null = null;
|
||||||
|
|
||||||
// Mock-Funktion für Google Login
|
ngOnInit() {
|
||||||
loginWithGoogle() {
|
// Überprüfen des Login-Status beim Start der Anwendung
|
||||||
// Hier würde die eigentliche Google Login-Logik stehen
|
const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
|
||||||
// Zum Beispiel mit Angular Fire oder einer anderen Bibliothek
|
const accessToken = localStorage.getItem('accessToken');
|
||||||
// Für dieses Beispiel simulieren wir den Login:
|
const refreshToken = localStorage.getItem('refreshToken');
|
||||||
this.isLoggedIn = true;
|
this.photoURL = localStorage.getItem('photoURL');
|
||||||
console.log('Logged in with Google');
|
this.showDropdown = false;
|
||||||
|
if (isLoggedIn && accessToken && refreshToken) {
|
||||||
|
this.isLoggedIn = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loginWithGoogle() {
|
||||||
|
const provider = new GoogleAuthProvider();
|
||||||
|
try {
|
||||||
|
const result = await signInWithPopup(this.auth, provider);
|
||||||
|
this.isLoggedIn = true;
|
||||||
|
this.photoURL = result.user.photoURL;
|
||||||
|
|
||||||
|
// Speichern des Login-Status und Tokens im Local Storage
|
||||||
|
localStorage.setItem('isLoggedIn', 'true');
|
||||||
|
localStorage.setItem('accessToken', await result.user.getIdToken());
|
||||||
|
localStorage.setItem('refreshToken', result.user.refreshToken);
|
||||||
|
localStorage.setItem('photoURL', result.user.photoURL);
|
||||||
|
|
||||||
|
console.log('Logged in with Google', result.user);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Google Login failed', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
this.isLoggedIn = false;
|
||||||
|
localStorage.removeItem('isLoggedIn');
|
||||||
|
localStorage.removeItem('accessToken');
|
||||||
|
localStorage.removeItem('refreshToken');
|
||||||
|
// Optional: Firebase-Logout durchführen
|
||||||
|
this.auth.signOut();
|
||||||
|
}
|
||||||
|
toggleDropdown() {
|
||||||
|
this.showDropdown = !this.showDropdown;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,25 @@
|
||||||
|
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||||
|
import { initializeApp, provideFirebaseApp } from '@angular/fire/app';
|
||||||
|
import { getAuth, provideAuth } from '@angular/fire/auth';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
import { provideHttpClient } from '@angular/common/http';
|
import { environment } from './environments/environment';
|
||||||
|
import { authInterceptor } from './service/auth.interceptor';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes),provideHttpClient()]
|
providers: [
|
||||||
|
// {
|
||||||
|
// provide: HTTP_INTERCEPTORS,
|
||||||
|
// useClass: AuthInterceptor,
|
||||||
|
// multi: true,
|
||||||
|
// },
|
||||||
|
provideFirebaseApp(() => initializeApp(environment.firebase)),
|
||||||
|
provideAuth(() => getAuth()),
|
||||||
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
|
provideRouter(routes),
|
||||||
|
provideHttpClient(
|
||||||
|
withInterceptors([authInterceptor]), // Hier wird der Interceptor registriert
|
||||||
|
),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import { Box, Deck, DeckImage, DeckService, OcrResult } from './deck.service';
|
||||||
import { EditImageModalComponent } from './edit-image-modal/edit-image-modal.component';
|
import { EditImageModalComponent } from './edit-image-modal/edit-image-modal.component';
|
||||||
import { MoveImageModalComponent } from './move-image-modal/move-image-modal.component';
|
import { MoveImageModalComponent } from './move-image-modal/move-image-modal.component';
|
||||||
import { TrainingComponent } from './training/training.component';
|
import { TrainingComponent } from './training/training.component';
|
||||||
import { UploadImageModalComponent } from './upload-image-modal/upload-image-modal.component';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-deck-list',
|
selector: 'app-deck-list',
|
||||||
|
|
@ -26,11 +25,9 @@ import { UploadImageModalComponent } from './upload-image-modal/upload-image-mod
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
CreateDeckModalComponent,
|
CreateDeckModalComponent,
|
||||||
UploadImageModalComponent,
|
|
||||||
TrainingComponent,
|
TrainingComponent,
|
||||||
EditImageModalComponent,
|
EditImageModalComponent,
|
||||||
MoveImageModalComponent, // Adding the new component
|
MoveImageModalComponent, // Adding the new component
|
||||||
UploadImageModalComponent,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DeckListComponent implements OnInit {
|
export class DeckListComponent implements OnInit {
|
||||||
|
|
@ -43,10 +40,7 @@ export class DeckListComponent implements OnInit {
|
||||||
|
|
||||||
@ViewChild(CreateDeckModalComponent)
|
@ViewChild(CreateDeckModalComponent)
|
||||||
createDeckModal!: CreateDeckModalComponent;
|
createDeckModal!: CreateDeckModalComponent;
|
||||||
@ViewChild(UploadImageModalComponent)
|
|
||||||
uploadImageModal!: UploadImageModalComponent;
|
|
||||||
@ViewChild(EditImageModalComponent) editModal!: EditImageModalComponent;
|
@ViewChild(EditImageModalComponent) editModal!: EditImageModalComponent;
|
||||||
@ViewChild(UploadImageModalComponent) uploadModal!: UploadImageModalComponent;
|
|
||||||
@ViewChild('imageFile') imageFileElement!: ElementRef;
|
@ViewChild('imageFile') imageFileElement!: ElementRef;
|
||||||
|
|
||||||
imageData: {
|
imageData: {
|
||||||
|
|
@ -260,12 +254,6 @@ export class DeckListComponent implements OnInit {
|
||||||
this.createDeckModal.open();
|
this.createDeckModal.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method to open the upload image modal
|
|
||||||
openUploadImageModal(deckName: string): void {
|
|
||||||
this.uploadImageModal.deckName = deckName;
|
|
||||||
this.uploadImageModal.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method to check if a deck is expanded
|
// Method to check if a deck is expanded
|
||||||
isDeckExpanded(deckName: string): boolean {
|
isDeckExpanded(deckName: string): boolean {
|
||||||
return this.expandedDecks.has(deckName);
|
return this.expandedDecks.has(deckName);
|
||||||
|
|
@ -293,11 +281,6 @@ export class DeckListComponent implements OnInit {
|
||||||
sessionStorage.setItem('expandedDecks', JSON.stringify(expandedArray));
|
sessionStorage.setItem('expandedDecks', JSON.stringify(expandedArray));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method to open the upload modal
|
|
||||||
openUploadModal(): void {
|
|
||||||
this.uploadImageModal.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handler for the imageUploaded event
|
// Handler for the imageUploaded event
|
||||||
onImageUploaded(imageData: any): void {
|
onImageUploaded(imageData: any): void {
|
||||||
this.imageData = imageData;
|
this.imageData = imageData;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
export const environment = {
|
||||||
|
production: true,
|
||||||
|
firebase: {
|
||||||
|
apiKey: 'AIzaSyBBH7mGJtwY-6_x0kCmyWCGe6JCesRS49k',
|
||||||
|
authDomain: 'haiki-452bd.firebaseapp.com',
|
||||||
|
projectId: 'haiki-452bd',
|
||||||
|
storageBucket: 'haiki-452bd.firebasestorage.app',
|
||||||
|
messagingSenderId: '263288723576',
|
||||||
|
appId: '1:263288723576:web:2bc87146ef52d276f5358d',
|
||||||
|
measurementId: 'G-C1C3N16KB3',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
export const environment = {
|
||||||
|
production: false,
|
||||||
|
firebase: {
|
||||||
|
apiKey: 'AIzaSyBBH7mGJtwY-6_x0kCmyWCGe6JCesRS49k',
|
||||||
|
authDomain: 'haiki-452bd.firebaseapp.com',
|
||||||
|
projectId: 'haiki-452bd',
|
||||||
|
storageBucket: 'haiki-452bd.firebasestorage.app',
|
||||||
|
messagingSenderId: '263288723576',
|
||||||
|
appId: '1:263288723576:web:2bc87146ef52d276f5358d',
|
||||||
|
measurementId: 'G-C1C3N16KB3',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { HttpInterceptorFn } from '@angular/common/http';
|
||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { Auth } from '@angular/fire/auth';
|
||||||
|
import { from } from 'rxjs';
|
||||||
|
import { catchError, switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
||||||
|
const auth = inject(Auth); // Injizieren Sie den Auth-Dienst
|
||||||
|
const token = localStorage.getItem('accessToken');
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
const decodedToken = decodeToken(token);
|
||||||
|
const expirationTime = decodedToken.exp * 1000; // Umwandeln in Millisekunden
|
||||||
|
const currentTime = Date.now();
|
||||||
|
|
||||||
|
if (currentTime > expirationTime) {
|
||||||
|
// Token ist abgelaufen, erneuern Sie es
|
||||||
|
return from(refreshToken(auth)).pipe(
|
||||||
|
switchMap(newToken => {
|
||||||
|
const clonedReq = req.clone({
|
||||||
|
setHeaders: {
|
||||||
|
Authorization: `Bearer ${newToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return next(clonedReq);
|
||||||
|
}),
|
||||||
|
catchError(error => {
|
||||||
|
console.error('Failed to refresh token', error);
|
||||||
|
return next(req); // Ohne Token fortfahren
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Token ist gültig
|
||||||
|
const clonedReq = req.clone({
|
||||||
|
setHeaders: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return next(clonedReq);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return next(req);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function refreshToken(auth: Auth): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const unsubscribe = auth.onAuthStateChanged(async user => {
|
||||||
|
if (user) {
|
||||||
|
try {
|
||||||
|
const newToken = await user.getIdToken(true); // Token erneuern
|
||||||
|
localStorage.setItem('accessToken', newToken);
|
||||||
|
resolve(newToken);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to refresh token', error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(new Error('No authenticated user found'));
|
||||||
|
}
|
||||||
|
unsubscribe(); // Abonnement beenden
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeToken(token: string): any {
|
||||||
|
try {
|
||||||
|
return JSON.parse(atob(token.split('.')[1]));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error decoding token', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Directive, ElementRef, EventEmitter, HostListener, Output } from '@angular/core';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[appClickOutside]',
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class ClickOutsideDirective {
|
||||||
|
@Output() clickOutside = new EventEmitter<void>();
|
||||||
|
|
||||||
|
constructor(private elementRef: ElementRef) {}
|
||||||
|
|
||||||
|
@HostListener('document:click', ['$event.target'])
|
||||||
|
onClick(target: HTMLElement) {
|
||||||
|
const clickedInside = this.elementRef.nativeElement.contains(target);
|
||||||
|
if (!clickedInside) {
|
||||||
|
this.clickOutside.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
<div #uploadImageModal id="uploadImageModal" tabindex="-1" aria-hidden="true" class="fixed top-0 left-0 right-0 z-50 hidden w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-modal md:h-full">
|
|
||||||
<div class="relative h-full contents">
|
|
||||||
<div class="relative bg-white rounded-lg shadow">
|
|
||||||
<button type="button" class="absolute top-3 right-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center" (click)="closeModal()">
|
|
||||||
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
|
||||||
</svg>
|
|
||||||
<span class="sr-only">Close</span>
|
|
||||||
</button>
|
|
||||||
<div class="p-6 relative">
|
|
||||||
<h3 class="mb-4 text-xl font-medium text-gray-900">Add Image to Deck</h3>
|
|
||||||
|
|
||||||
<!-- Upload form -->
|
|
||||||
<div class="mb-4">
|
|
||||||
<!-- <label for="imageFile" class="block text-sm font-medium text-gray-700">Upload Image</label> -->
|
|
||||||
<!-- <input #imageFile type="file" id="imageFile" (change)="onFileChange($event)" accept="image/*" required class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" /> -->
|
|
||||||
<div class="relative">
|
|
||||||
<!-- Benutzerdefinierte Schaltfläche und Text -->
|
|
||||||
<label for="imageFile" class="block w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 cursor-pointer text-center">
|
|
||||||
Choose File
|
|
||||||
</label>
|
|
||||||
<!-- Das eigentliche Datei-Input-Feld -->
|
|
||||||
<input #imageFile type="file" id="imageFile" (change)="onFileChange($event)" accept="image/*" required class="hidden" />
|
|
||||||
<!-- Anzeige des ausgewählten Dateinamens -->
|
|
||||||
<!-- <span id="fileName" class="block mt-2 text-sm text-gray-700 text-center">No file chosen</span> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Status display -->
|
|
||||||
<div *ngIf="processingStatus" class="mt-4">
|
|
||||||
<p class="text-sm text-gray-700">{{ processingStatus }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Loading overlay -->
|
|
||||||
<div *ngIf="loading" class="absolute inset-0 bg-gray-800 bg-opacity-50 flex items-center justify-center z-10">
|
|
||||||
<div class="bg-white p-4 rounded shadow">
|
|
||||||
<p class="text-sm text-gray-700">Processing in progress...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
import { Component, Input, Output, EventEmitter, AfterViewInit, ViewChild, ElementRef, OnDestroy } from '@angular/core';
|
|
||||||
import { Box, DeckImage, DeckService, OcrResult } from '../deck.service';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Modal } from 'flowbite';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-upload-image-modal',
|
|
||||||
templateUrl: './upload-image-modal.component.html',
|
|
||||||
standalone: true,
|
|
||||||
styles:`
|
|
||||||
label {
|
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
label:hover {
|
|
||||||
background-color: #2563eb;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
imports: [CommonModule]
|
|
||||||
})
|
|
||||||
export class UploadImageModalComponent implements AfterViewInit, OnDestroy {
|
|
||||||
@Input() deckName: string = '';
|
|
||||||
@Output() imageUploaded = new EventEmitter<{ imageSrc: string | ArrayBuffer | null | undefined, deckImage: DeckImage }>();
|
|
||||||
|
|
||||||
@ViewChild('uploadImageModal') modalElement!: ElementRef;
|
|
||||||
@ViewChild('imageFile') imageFileElement!: ElementRef;
|
|
||||||
|
|
||||||
imageFile: File | null = null;
|
|
||||||
processingStatus: string = '';
|
|
||||||
loading: boolean = false;
|
|
||||||
modal: any;
|
|
||||||
|
|
||||||
constructor(private deckService: DeckService, private http: HttpClient) { }
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
|
||||||
this.modal = new Modal(this.modalElement.nativeElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
// Modal is automatically managed by Flowbite
|
|
||||||
}
|
|
||||||
|
|
||||||
open(): void {
|
|
||||||
this.resetState();
|
|
||||||
this.modal.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
closeModal(): void {
|
|
||||||
this.modal.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
resetState(): void {
|
|
||||||
this.imageFile = null;
|
|
||||||
this.processingStatus = '';
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
onFileChange(event: any): void {
|
|
||||||
const file: File = event.target.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
const fileNameElement = document.getElementById('fileName');
|
|
||||||
if (fileNameElement) {
|
|
||||||
fileNameElement.textContent = file.name;
|
|
||||||
}
|
|
||||||
this.imageFile = file;
|
|
||||||
this.processingStatus = 'Processing in progress...';
|
|
||||||
this.loading = true;
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = async (e) => {
|
|
||||||
const imageSrc = e.target?.result;
|
|
||||||
|
|
||||||
// Image as Base64 string without prefix (data:image/...)
|
|
||||||
const imageBase64 = (imageSrc as string).split(',')[1];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this.http.post<any>('/api/ocr', { image: imageBase64 }).toPromise();
|
|
||||||
|
|
||||||
if (!response || !response.results) {
|
|
||||||
this.processingStatus = 'Invalid response from OCR service';
|
|
||||||
this.loading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.processingStatus = 'Processing complete';
|
|
||||||
this.loading = false;
|
|
||||||
|
|
||||||
// Emit event with image data and OCR results
|
|
||||||
const imageName = this.imageFile?.name ?? '';
|
|
||||||
const imageId = response.results.length > 0 ? response.results[0].name : null;
|
|
||||||
const boxes: Box[] = [];
|
|
||||||
response.results.forEach((result: OcrResult) => {
|
|
||||||
const box = result.box;
|
|
||||||
|
|
||||||
const xs = box.map((point: number[]) => point[0]);
|
|
||||||
const ys = box.map((point: number[]) => point[1]);
|
|
||||||
const xMin = Math.min(...xs);
|
|
||||||
const xMax = Math.max(...xs);
|
|
||||||
const yMin = Math.min(...ys);
|
|
||||||
const yMax = Math.max(...ys);
|
|
||||||
boxes.push({ x1: xMin, x2: xMax, y1: yMin, y2: yMax });
|
|
||||||
});
|
|
||||||
const deckImage: DeckImage = { name: imageName, id: imageId, boxes };
|
|
||||||
this.imageUploaded.emit({ imageSrc, deckImage });
|
|
||||||
this.resetFileInput();
|
|
||||||
// Close the upload modal
|
|
||||||
this.closeModal();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error with OCR service:', error);
|
|
||||||
this.processingStatus = 'Error with OCR service';
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the file input field so the same file can be selected again.
|
|
||||||
*/
|
|
||||||
resetFileInput(): void {
|
|
||||||
if (this.imageFileElement && this.imageFileElement.nativeElement) {
|
|
||||||
this.imageFileElement.nativeElement.value = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue