From d180cd70e821d1b1847f40d1d43c9cf5f710595f Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Thu, 13 Feb 2025 10:56:03 -0600 Subject: [PATCH] #23: users table + insert/update added --- api/.env | 3 +- api/src/app/app.module.ts | 3 +- api/src/app/drizzle.service.ts | 49 +++++++++++++++---- api/src/app/user.controller.ts | 15 ++++++ api/src/db/schema.ts | 9 ++-- api/tsconfig.app.json | 1 + ...rd_cuckoos.sql => 0000_fuzzy_invaders.sql} | 3 +- drizzle/meta/0000_snapshot.json | 6 +-- drizzle/meta/_journal.json | 4 +- project.json | 3 +- proxy.conf.json | 2 +- src/app/app.component.ts | 9 ++-- .../create-deck-modal.component.ts | 2 +- src/app/deck-list.component.ts | 2 +- .../edit-image-modal.component.ts | 2 +- .../move-image-modal.component.ts | 2 +- src/app/{ => services}/deck.service.ts | 0 src/app/services/user.service.ts | 26 ++++++++++ src/app/training/training.component.ts | 2 +- 19 files changed, 110 insertions(+), 33 deletions(-) create mode 100644 api/src/app/user.controller.ts rename drizzle/{0000_known_stepford_cuckoos.sql => 0000_fuzzy_invaders.sql} (88%) rename src/app/{ => services}/deck.service.ts (100%) create mode 100644 src/app/services/user.service.ts diff --git a/api/.env b/api/.env index a32b978..f1d207d 100644 --- a/api/.env +++ b/api/.env @@ -1,5 +1,6 @@ DB_FILE_NAME=file:local.db -PORT=3000 +PORT=3002 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 +DATABASE_URL=postgresql://haiky:xieng7Seih@localhost:15432/haiky \ No newline at end of file diff --git a/api/src/app/app.module.ts b/api/src/app/app.module.ts index 7dddec0..22a76bd 100644 --- a/api/src/app/app.module.ts +++ b/api/src/app/app.module.ts @@ -3,10 +3,11 @@ 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], + controllers: [DecksController, ProxyController, UserController], providers: [DrizzleService, SqlLoggerService], }) export class AppModule {} diff --git a/api/src/app/drizzle.service.ts b/api/src/app/drizzle.service.ts index a9c0978..9151b8b 100644 --- a/api/src/app/drizzle.service.ts +++ b/api/src/app/drizzle.service.ts @@ -3,7 +3,7 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { and, eq, sql } from 'drizzle-orm'; //import { drizzle } from 'drizzle-orm/libsql'; import { drizzle } from 'drizzle-orm/node-postgres'; -import { deck, SelectDeck, User } from '../db/schema'; +import { deck, InsertUser, SelectDeck, User, users } from '../db/schema'; import { SqlLoggerService } from './sql-logger.service'; @Injectable() @@ -11,16 +11,7 @@ export class DrizzleService { // private readonly logger = new Logger(DrizzleService.name); private db: any; constructor(private sqlLogger: SqlLoggerService) { - // this.db = drizzle('file:local.db', { - // logger: { - // logQuery: (query: string, params: any[]) => { - // this.sqlLogger.logQuery(query, params); - // }, - // }, - // }); - this.db = drizzle(process.env['DATABASE_URL']!, { - //this.db = drizzle('postgresql://haiky:xieng7Seih@localhost:15432/haiky', { logger: { logQuery: (query: string, params: any[]) => { this.sqlLogger.logQuery(query, params); @@ -296,4 +287,42 @@ export class DrizzleService { 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 mit `role: 'guest'` und `lastLogin` auf den aktuellen Zeitpunkt angelegt. + */ + async logIn(createUserDto: InsertUser) { + // Prüfen, ob der Benutzer bereits existiert (hier anhand der E-Mail) + const existingUser = await this.db.select().from(users).where(eq(users.email, createUserDto.email)).limit(1); + + if (existingUser.length > 0) { + // Benutzer existiert: Update des letzten Logins und ggf. weiterer Felder + const updatedUser = await this.db + .update(users) + .set({ + lastLogin: new Date(), // Setzt lastLogin explizit auf den aktuellen Zeitpunkt + // Optional: Aktualisierung von Name und sign_in_provider, falls sich diese ändern sollten + name: createUserDto.name, + sign_in_provider: createUserDto.sign_in_provider, + }) + .where(eq(users.email, createUserDto.email)) + .returning(); + return updatedUser; + } else { + // Neuer Benutzer: Insert mit role per Default 'guest' und lastLogin auf now + const insertedUser = await this.db + .insert(users) + .values({ + name: createUserDto.name, + email: createUserDto.email, + sign_in_provider: createUserDto.sign_in_provider, + lastLogin: new Date(), // Setzt lastLogin auf now + role: 'guest', // Default-Wert 'guest' + }) + .returning(); + return insertedUser; + } + } } diff --git a/api/src/app/user.controller.ts b/api/src/app/user.controller.ts new file mode 100644 index 0000000..91217dc --- /dev/null +++ b/api/src/app/user.controller.ts @@ -0,0 +1,15 @@ +// user.controller.ts +import { Body, Controller, Post } from '@nestjs/common'; +import type { InsertUser } from '../db/schema'; +import { DrizzleService } from './drizzle.service'; + +@Controller('users') +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); + } +} diff --git a/api/src/db/schema.ts b/api/src/db/schema.ts index 6d04d8b..dcc0bef 100644 --- a/api/src/db/schema.ts +++ b/api/src/db/schema.ts @@ -26,6 +26,8 @@ export const deck = table( }, 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', { @@ -34,13 +36,12 @@ export const users = table( email: t.varchar().notNull(), role: rolesEnum().default('guest'), sign_in_provider: t.varchar('sign_in_provider', { length: 50 }), - inserted: t.timestamp('inserted', { mode: 'date' }).defaultNow(), - updated: t.timestamp('updated', { mode: 'date' }).defaultNow(), + lastLogin: t.timestamp('lastLogin', { mode: 'date' }).defaultNow(), }, table => [t.uniqueIndex('users_idx').on(table.id)], ); -export type InsertDeck = typeof deck.$inferInsert; -export type SelectDeck = typeof deck.$inferSelect; +export type InsertUser = typeof users.$inferInsert; +export type SelectUser = typeof users.$inferSelect; export interface User { name: string; picture: string; diff --git a/api/tsconfig.app.json b/api/tsconfig.app.json index 464d9bd..56ca835 100644 --- a/api/tsconfig.app.json +++ b/api/tsconfig.app.json @@ -5,6 +5,7 @@ "module": "ES2020", "types": ["node"], "emitDecoratorMetadata": true, + "isolatedModules": true, "target": "es2021", "strictNullChecks": true }, diff --git a/drizzle/0000_known_stepford_cuckoos.sql b/drizzle/0000_fuzzy_invaders.sql similarity index 88% rename from drizzle/0000_known_stepford_cuckoos.sql rename to drizzle/0000_fuzzy_invaders.sql index 5544a5f..b3c05ca 100644 --- a/drizzle/0000_known_stepford_cuckoos.sql +++ b/drizzle/0000_fuzzy_invaders.sql @@ -25,8 +25,7 @@ CREATE TABLE "users" ( "email" varchar NOT NULL, "role" "roles" DEFAULT 'guest', "sign_in_provider" varchar(50), - "inserted" timestamp DEFAULT now(), - "updated" timestamp DEFAULT now() + "lastLogin" timestamp DEFAULT now() ); --> statement-breakpoint CREATE UNIQUE INDEX "deck_idx" ON "deck" USING btree ("id");--> statement-breakpoint diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index f099ef2..d98a508 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "5c983d57-b2f3-4fb4-b99e-066574c486e3", + "id": "2bed4eae-3e6e-414b-85e2-d16e632c17ef", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -202,8 +202,8 @@ "notNull": false, "default": "now()" }, - "updated": { - "name": "updated", + "lastLogin": { + "name": "lastLogin", "type": "timestamp", "primaryKey": false, "notNull": false, diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index a603d2a..a7bf69e 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "7", - "when": 1738963581892, - "tag": "0000_known_stepford_cuckoos", + "when": 1739461160843, + "tag": "0000_fuzzy_invaders", "breakpoints": true } ] diff --git a/project.json b/project.json index 1c53840..337d26c 100644 --- a/project.json +++ b/project.json @@ -90,7 +90,8 @@ "serve": { "executor": "@angular/build:dev-server", "options": { - "proxyConfig": "proxy.conf.json" + "proxyConfig": "proxy.conf.json", + "port": 4202 }, "configurations": { "production": { diff --git a/proxy.conf.json b/proxy.conf.json index ee6e920..88a66c8 100644 --- a/proxy.conf.json +++ b/proxy.conf.json @@ -1,6 +1,6 @@ { "/api": { - "target": "http://localhost:3000", + "target": "http://localhost:3002", "secure": false, "changeOrigin": true }, diff --git a/src/app/app.component.ts b/src/app/app.component.ts index df7e4d0..05349be 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,11 +1,12 @@ import { CommonModule } from '@angular/common'; import { Component, inject } from '@angular/core'; import { Auth } from '@angular/fire/auth'; -import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth'; +import { GoogleAuthProvider, signInWithPopup, UserCredential } from 'firebase/auth'; import { PopoverComponent } from './components/popover.component'; import { DeckListComponent } from './deck-list.component'; import { ClickOutsideDirective } from './service/click-outside.directive'; import { PopoverService } from './services/popover.service'; +import { UserService } from './services/user.service'; @Component({ selector: 'app-root', template: ` @@ -119,7 +120,7 @@ export class AppComponent { private confirmCallback?: (inputValue?: string) => void; private cancelCallback?: () => void; - constructor(public popoverService: PopoverService) { + constructor(public popoverService: PopoverService, private userService: UserService) { this.popoverService.popoverState$.subscribe(options => { this.popoverVisible = true; this.popoverTitle = options.title; @@ -147,7 +148,7 @@ export class AppComponent { async loginWithGoogle() { const provider = new GoogleAuthProvider(); try { - const result = await signInWithPopup(this.auth, provider); + const result: UserCredential = await signInWithPopup(this.auth, provider); this.isLoggedIn = true; this.photoURL = result.user.photoURL; @@ -159,6 +160,8 @@ export class AppComponent { this.showDropdown = false; + await this.userService.logIn({ name: result.user.displayName, email: result.user.email, sign_in_provider: result.providerId }); + console.log('Logged in with Google', result.user); } catch (error) { console.error('Google Login failed', error); diff --git a/src/app/create-deck-modal/create-deck-modal.component.ts b/src/app/create-deck-modal/create-deck-modal.component.ts index fc4470a..ebbcaf0 100644 --- a/src/app/create-deck-modal/create-deck-modal.component.ts +++ b/src/app/create-deck-modal/create-deck-modal.component.ts @@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'; import { AfterViewInit, Component, ElementRef, EventEmitter, Output, ViewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { Modal } from 'flowbite'; -import { DeckService } from '../deck.service'; +import { DeckService } from '../services/deck.service'; import { PopoverService } from '../services/popover.service'; @Component({ diff --git a/src/app/deck-list.component.ts b/src/app/deck-list.component.ts index 8130739..ac53329 100644 --- a/src/app/deck-list.component.ts +++ b/src/app/deck-list.component.ts @@ -4,9 +4,9 @@ import { ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild } from '@an import { FormsModule } from '@angular/forms'; import { firstValueFrom } from 'rxjs'; import { CreateDeckModalComponent } from './create-deck-modal/create-deck-modal.component'; -import { Box, Deck, DeckImage, DeckService, OcrResult } from './deck.service'; import { EditImageModalComponent } from './edit-image-modal/edit-image-modal.component'; import { MoveImageModalComponent } from './move-image-modal/move-image-modal.component'; +import { Box, Deck, DeckImage, DeckService, OcrResult } from './services/deck.service'; import { PopoverService } from './services/popover.service'; import { TrainingComponent } from './training/training.component'; diff --git a/src/app/edit-image-modal/edit-image-modal.component.ts b/src/app/edit-image-modal/edit-image-modal.component.ts index c8dea92..74f38d4 100644 --- a/src/app/edit-image-modal/edit-image-modal.component.ts +++ b/src/app/edit-image-modal/edit-image-modal.component.ts @@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'; import { AfterViewInit, Component, ElementRef, EventEmitter, Input, OnDestroy, Output, ViewChild } from '@angular/core'; import { fabric } from 'fabric'; import { Modal } from 'flowbite'; -import { DeckImage, DeckService } from '../deck.service'; +import { DeckImage, DeckService } from '../services/deck.service'; import { PopoverService } from '../services/popover.service'; @Component({ diff --git a/src/app/move-image-modal/move-image-modal.component.ts b/src/app/move-image-modal/move-image-modal.component.ts index 1d2e45d..6bb15eb 100644 --- a/src/app/move-image-modal/move-image-modal.component.ts +++ b/src/app/move-image-modal/move-image-modal.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common'; import { Component, EventEmitter, Input, Output } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { Deck, DeckImage, DeckService } from '../deck.service'; +import { Deck, DeckImage, DeckService } from '../services/deck.service'; import { PopoverService } from '../services/popover.service'; @Component({ diff --git a/src/app/deck.service.ts b/src/app/services/deck.service.ts similarity index 100% rename from src/app/deck.service.ts rename to src/app/services/deck.service.ts diff --git a/src/app/services/user.service.ts b/src/app/services/user.service.ts new file mode 100644 index 0000000..80bb9c9 --- /dev/null +++ b/src/app/services/user.service.ts @@ -0,0 +1,26 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { lastValueFrom } from 'rxjs'; + +export interface User { + name: string; + email: string; + sign_in_provider: string; +} + +@Injectable({ + providedIn: 'root', +}) +export class UserService { + // Passe die URL an, je nachdem wie dein Backend-Routing definiert ist. + private apiUrl = '/api/users'; + + constructor(private http: HttpClient) {} + + /** + * Sendet die Benutzerinformationen an das Backend. + */ + logIn(user: User): Promise { + return lastValueFrom(this.http.post(this.apiUrl, user)); + } +} diff --git a/src/app/training/training.component.ts b/src/app/training/training.component.ts index 44ded54..ce465b6 100644 --- a/src/app/training/training.component.ts +++ b/src/app/training/training.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common'; import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import { lastValueFrom } from 'rxjs'; -import { Box, Deck, DeckImage, DeckService } from '../deck.service'; +import { Box, Deck, DeckImage, DeckService } from '../services/deck.service'; import { PopoverService } from '../services/popover.service'; const LEARNING_STEPS = {