From 078dc8c304c64fe09db42ea2a0cca19d7dc4d33a Mon Sep 17 00:00:00 2001 From: "simon.franken" Date: Mon, 23 Feb 2026 11:39:09 +0100 Subject: [PATCH] Add Prisma session store for persistent sessions --- backend/package-lock.json | 76 +++++++++++++++++++ backend/package.json | 1 + .../migration.sql | 12 +++ backend/prisma/schema.prisma | 51 ++++++++----- backend/src/index.ts | 8 +- 5 files changed, 126 insertions(+), 22 deletions(-) create mode 100644 backend/prisma/migrations/20260223103350_add_session_model/migration.sql diff --git a/backend/package-lock.json b/backend/package-lock.json index fdf4b21..f27e794 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@prisma/client": "^6.19.2", + "@quixo3/prisma-session-store": "^3.1.19", "cors": "^2.8.5", "dotenv": "^17.3.1", "express": "^4.18.2", @@ -470,6 +471,27 @@ "node": ">=18" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@prisma/client": { "version": "6.19.2", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.2.tgz", @@ -555,6 +577,24 @@ "@prisma/debug": "6.19.2" } }, + "node_modules/@quixo3/prisma-session-store": { + "version": "3.1.19", + "resolved": "https://registry.npmjs.org/@quixo3/prisma-session-store/-/prisma-session-store-3.1.19.tgz", + "integrity": "sha512-fCG7dzmd8dyqoj4XSi5IHETqrbzN+roz4+4pPS1uMo0kVQu8CT9HRbULuIaOxWCAODT7yGyNGNvVywEeGI80lw==", + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.0", + "ts-dedent": "^2.2.0", + "type-fest": "^5.3.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "@prisma/client": ">=2.16.1", + "express-session": ">=1.17.1" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -2133,6 +2173,18 @@ "node": ">= 0.8" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tinyexec": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", @@ -2152,6 +2204,15 @@ "node": ">=0.6" } }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -2172,6 +2233,21 @@ "fsevents": "~2.3.3" } }, + "node_modules/type-fest": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz", + "integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/backend/package.json b/backend/package.json index eea2416..41bd041 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@prisma/client": "^6.19.2", + "@quixo3/prisma-session-store": "^3.1.19", "cors": "^2.8.5", "dotenv": "^17.3.1", "express": "^4.18.2", diff --git a/backend/prisma/migrations/20260223103350_add_session_model/migration.sql b/backend/prisma/migrations/20260223103350_add_session_model/migration.sql new file mode 100644 index 0000000..617bdde --- /dev/null +++ b/backend/prisma/migrations/20260223103350_add_session_model/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE "sessions" ( + "id" TEXT NOT NULL, + "sid" TEXT NOT NULL, + "data" TEXT NOT NULL, + "expires_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "sessions_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "sessions_sid_key" ON "sessions"("sid"); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index fb41269..daedaf2 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -10,7 +10,7 @@ datasource db { model User { id String @id @db.VarChar(255) username String @db.VarChar(255) - fullName String? @db.VarChar(255) @map("full_name") + fullName String? @map("full_name") @db.VarChar(255) email String @db.VarChar(255) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@ -31,9 +31,9 @@ model Client { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") - userId String @map("user_id") @db.VarChar(255) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - projects Project[] + userId String @map("user_id") @db.VarChar(255) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + projects Project[] clientTargets ClientTarget[] @@index([userId]) @@ -41,10 +41,10 @@ model Client { } model Project { - id String @id @default(uuid()) - name String @db.VarChar(255) - description String? @db.Text - color String? @db.VarChar(7) // Hex color code + id String @id @default(uuid()) + name String @db.VarChar(255) + description String? @db.Text + color String? @db.VarChar(7) // Hex color code createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@ -53,7 +53,7 @@ model Project { clientId String @map("client_id") client Client @relation(fields: [clientId], references: [id], onDelete: Cascade) - timeEntries TimeEntry[] + timeEntries TimeEntry[] ongoingTimers OngoingTimer[] @@index([userId]) @@ -69,9 +69,9 @@ model TimeEntry { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") - userId String @map("user_id") @db.VarChar(255) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - projectId String @map("project_id") + userId String @map("user_id") @db.VarChar(255) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + projectId String @map("project_id") project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) @@index([userId]) @@ -86,9 +86,9 @@ model OngoingTimer { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") - userId String @map("user_id") @db.VarChar(255) @unique - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - projectId String? @map("project_id") + userId String @unique @map("user_id") @db.VarChar(255) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + projectId String? @map("project_id") project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull) @@index([userId]) @@ -96,11 +96,11 @@ model OngoingTimer { } model ClientTarget { - id String @id @default(uuid()) - weeklyHours Float @map("weekly_hours") - startDate DateTime @map("start_date") @db.Date // Always a Monday - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + id String @id @default(uuid()) + weeklyHours Float @map("weekly_hours") + startDate DateTime @map("start_date") @db.Date // Always a Monday + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") userId String @map("user_id") @db.VarChar(255) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@ -128,4 +128,13 @@ model BalanceCorrection { @@index([clientTargetId]) @@map("balance_corrections") -} \ No newline at end of file +} + +model Session { + id String @id + sid String @unique + data String @db.Text + expiresAt DateTime @map("expires_at") + + @@map("sessions") +} diff --git a/backend/src/index.ts b/backend/src/index.ts index 56772ce..e41bc5b 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,8 +1,9 @@ import express from "express"; import cors from "cors"; import session from "express-session"; +import { PrismaSessionStore } from "@quixo3/prisma-session-store"; import { config, validateConfig } from "./config"; -import { connectDatabase } from "./prisma/client"; +import { connectDatabase, prisma } from "./prisma/client"; import { errorHandler, notFoundHandler } from "./middleware/errorHandler"; // Import routes @@ -43,6 +44,11 @@ async function main() { resave: false, saveUninitialized: false, name: "sessionId", + store: new PrismaSessionStore(prisma, { + checkPeriod: 2 * 60 * 1000, // ms + dbRecordIdIsSessionId: true, + dbRecordIdFunction: undefined, + }), cookie: { secure: config.nodeEnv === "production", httpOnly: true, -- 2.49.1