#23: users table + insert/update added

This commit is contained in:
Andreas Knuth 2025-02-13 10:56:03 -06:00
parent 5eee7c9ac4
commit d180cd70e8
19 changed files with 110 additions and 33 deletions

View File

@ -1,5 +1,6 @@
DB_FILE_NAME=file:local.db DB_FILE_NAME=file:local.db
PORT=3000 PORT=3002
FIREBASE_PROJECT_ID=haiki-452bd FIREBASE_PROJECT_ID=haiki-452bd
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-fbsvc@haiki-452bd.iam.gserviceaccount.com 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 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

View File

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

View File

@ -3,7 +3,7 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { and, eq, sql } from 'drizzle-orm'; import { and, eq, sql } from 'drizzle-orm';
//import { drizzle } from 'drizzle-orm/libsql'; //import { drizzle } from 'drizzle-orm/libsql';
import { drizzle } from 'drizzle-orm/node-postgres'; 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'; import { SqlLoggerService } from './sql-logger.service';
@Injectable() @Injectable()
@ -11,16 +11,7 @@ export class DrizzleService {
// private readonly logger = new Logger(DrizzleService.name); // private readonly logger = new Logger(DrizzleService.name);
private db: any; private db: any;
constructor(private sqlLogger: SqlLoggerService) { 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(process.env['DATABASE_URL']!, {
//this.db = drizzle('postgresql://haiky:xieng7Seih@localhost:15432/haiky', {
logger: { logger: {
logQuery: (query: string, params: any[]) => { logQuery: (query: string, params: any[]) => {
this.sqlLogger.logQuery(query, params); 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); 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;
}
}
} }

View File

@ -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);
}
}

View File

@ -26,6 +26,8 @@ export const deck = table(
}, },
table => [t.uniqueIndex('deck_idx').on(table.id)], table => [t.uniqueIndex('deck_idx').on(table.id)],
); );
export type InsertDeck = typeof deck.$inferInsert;
export type SelectDeck = typeof deck.$inferSelect;
export const users = table( export const users = table(
'users', 'users',
{ {
@ -34,13 +36,12 @@ export const users = table(
email: t.varchar().notNull(), email: t.varchar().notNull(),
role: rolesEnum().default('guest'), role: rolesEnum().default('guest'),
sign_in_provider: t.varchar('sign_in_provider', { length: 50 }), sign_in_provider: t.varchar('sign_in_provider', { length: 50 }),
inserted: t.timestamp('inserted', { mode: 'date' }).defaultNow(), lastLogin: t.timestamp('lastLogin', { mode: 'date' }).defaultNow(),
updated: t.timestamp('updated', { mode: 'date' }).defaultNow(),
}, },
table => [t.uniqueIndex('users_idx').on(table.id)], table => [t.uniqueIndex('users_idx').on(table.id)],
); );
export type InsertDeck = typeof deck.$inferInsert; export type InsertUser = typeof users.$inferInsert;
export type SelectDeck = typeof deck.$inferSelect; export type SelectUser = typeof users.$inferSelect;
export interface User { export interface User {
name: string; name: string;
picture: string; picture: string;

View File

@ -5,6 +5,7 @@
"module": "ES2020", "module": "ES2020",
"types": ["node"], "types": ["node"],
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"isolatedModules": true,
"target": "es2021", "target": "es2021",
"strictNullChecks": true "strictNullChecks": true
}, },

View File

@ -25,8 +25,7 @@ CREATE TABLE "users" (
"email" varchar NOT NULL, "email" varchar NOT NULL,
"role" "roles" DEFAULT 'guest', "role" "roles" DEFAULT 'guest',
"sign_in_provider" varchar(50), "sign_in_provider" varchar(50),
"inserted" timestamp DEFAULT now(), "lastLogin" timestamp DEFAULT now()
"updated" timestamp DEFAULT now()
); );
--> statement-breakpoint --> statement-breakpoint
CREATE UNIQUE INDEX "deck_idx" ON "deck" USING btree ("id");--> statement-breakpoint CREATE UNIQUE INDEX "deck_idx" ON "deck" USING btree ("id");--> statement-breakpoint

View File

@ -1,5 +1,5 @@
{ {
"id": "5c983d57-b2f3-4fb4-b99e-066574c486e3", "id": "2bed4eae-3e6e-414b-85e2-d16e632c17ef",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"version": "7", "version": "7",
"dialect": "postgresql", "dialect": "postgresql",
@ -202,8 +202,8 @@
"notNull": false, "notNull": false,
"default": "now()" "default": "now()"
}, },
"updated": { "lastLogin": {
"name": "updated", "name": "lastLogin",
"type": "timestamp", "type": "timestamp",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,

View File

@ -5,8 +5,8 @@
{ {
"idx": 0, "idx": 0,
"version": "7", "version": "7",
"when": 1738963581892, "when": 1739461160843,
"tag": "0000_known_stepford_cuckoos", "tag": "0000_fuzzy_invaders",
"breakpoints": true "breakpoints": true
} }
] ]

View File

@ -90,7 +90,8 @@
"serve": { "serve": {
"executor": "@angular/build:dev-server", "executor": "@angular/build:dev-server",
"options": { "options": {
"proxyConfig": "proxy.conf.json" "proxyConfig": "proxy.conf.json",
"port": 4202
}, },
"configurations": { "configurations": {
"production": { "production": {

View File

@ -1,6 +1,6 @@
{ {
"/api": { "/api": {
"target": "http://localhost:3000", "target": "http://localhost:3002",
"secure": false, "secure": false,
"changeOrigin": true "changeOrigin": true
}, },

View File

@ -1,11 +1,12 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { Auth } from '@angular/fire/auth'; 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 { PopoverComponent } from './components/popover.component';
import { DeckListComponent } from './deck-list.component'; import { DeckListComponent } from './deck-list.component';
import { ClickOutsideDirective } from './service/click-outside.directive'; import { ClickOutsideDirective } from './service/click-outside.directive';
import { PopoverService } from './services/popover.service'; import { PopoverService } from './services/popover.service';
import { UserService } from './services/user.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
template: ` template: `
@ -119,7 +120,7 @@ export class AppComponent {
private confirmCallback?: (inputValue?: string) => void; private confirmCallback?: (inputValue?: string) => void;
private cancelCallback?: () => void; private cancelCallback?: () => void;
constructor(public popoverService: PopoverService) { constructor(public popoverService: PopoverService, private userService: UserService) {
this.popoverService.popoverState$.subscribe(options => { this.popoverService.popoverState$.subscribe(options => {
this.popoverVisible = true; this.popoverVisible = true;
this.popoverTitle = options.title; this.popoverTitle = options.title;
@ -147,7 +148,7 @@ export class AppComponent {
async loginWithGoogle() { async loginWithGoogle() {
const provider = new GoogleAuthProvider(); const provider = new GoogleAuthProvider();
try { try {
const result = await signInWithPopup(this.auth, provider); const result: UserCredential = await signInWithPopup(this.auth, provider);
this.isLoggedIn = true; this.isLoggedIn = true;
this.photoURL = result.user.photoURL; this.photoURL = result.user.photoURL;
@ -159,6 +160,8 @@ export class AppComponent {
this.showDropdown = false; 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); console.log('Logged in with Google', result.user);
} catch (error) { } catch (error) {
console.error('Google Login failed', error); console.error('Google Login failed', error);

View File

@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common';
import { AfterViewInit, Component, ElementRef, EventEmitter, Output, ViewChild } from '@angular/core'; import { AfterViewInit, Component, ElementRef, EventEmitter, Output, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { Modal } from 'flowbite'; import { Modal } from 'flowbite';
import { DeckService } from '../deck.service'; import { DeckService } from '../services/deck.service';
import { PopoverService } from '../services/popover.service'; import { PopoverService } from '../services/popover.service';
@Component({ @Component({

View File

@ -4,9 +4,9 @@ import { ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild } from '@an
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { CreateDeckModalComponent } from './create-deck-modal/create-deck-modal.component'; 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 { 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 { Box, Deck, DeckImage, DeckService, OcrResult } from './services/deck.service';
import { PopoverService } from './services/popover.service'; import { PopoverService } from './services/popover.service';
import { TrainingComponent } from './training/training.component'; import { TrainingComponent } from './training/training.component';

View File

@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
import { AfterViewInit, Component, ElementRef, EventEmitter, Input, OnDestroy, Output, ViewChild } from '@angular/core'; import { AfterViewInit, Component, ElementRef, EventEmitter, Input, OnDestroy, Output, ViewChild } from '@angular/core';
import { fabric } from 'fabric'; import { fabric } from 'fabric';
import { Modal } from 'flowbite'; import { Modal } from 'flowbite';
import { DeckImage, DeckService } from '../deck.service'; import { DeckImage, DeckService } from '../services/deck.service';
import { PopoverService } from '../services/popover.service'; import { PopoverService } from '../services/popover.service';
@Component({ @Component({

View File

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormsModule } from '@angular/forms'; 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'; import { PopoverService } from '../services/popover.service';
@Component({ @Component({

View File

@ -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<any> {
return lastValueFrom(this.http.post<any>(this.apiUrl, user));
}
}

View File

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { lastValueFrom } from 'rxjs'; 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'; import { PopoverService } from '../services/popover.service';
const LEARNING_STEPS = { const LEARNING_STEPS = {