Latest changes

This commit is contained in:
Dum 2026-03-01 19:31:22 +05:30
parent a0ffb32fd5
commit a5baed2ee4
23 changed files with 2525 additions and 1096 deletions

View file

@ -22,6 +22,9 @@ TMDB_API_KEY=''
TRAKT_CLIENT_ID=''
TRAKT_SECRET_ID=''
# Optional: PostgreSQL connection pool size (default: 100000)
# DB_POOL_MAX=100000
# Optional: Captcha
CAPTCHA=false
CAPTCHA_CLIENT_KEY=''

3135
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -15,18 +15,23 @@
"eslint-plugin-prettier": "^5.4.0",
"nitropack": "latest",
"prettier": "^3.5.3",
"prisma": "^7.0.1"
"prisma": "^7.0.1",
"rollup": "^4.59.0"
},
"dependencies": {
"@prisma/adapter-pg": "^7.0.1",
"@prisma/client": "^7.0.1",
"@types/pg": "^8.18.0",
"cheerio": "^1.0.0",
"dotenv": "^16.4.7",
"jsonwebtoken": "^9.0.2",
"p-limit": "^7.3.0",
"pg": "^8.19.0",
"prom-client": "^15.1.3",
"tmdb-ts": "^2.0.1",
"trakt.tv": "^8.2.0",
"tweetnacl": "^1.0.3",
"uuidv7": "^1.1.0",
"whatwg-url": "^14.2.0",
"zod": "^3.24.2"
}

View file

@ -1,7 +1,8 @@
generator client {
provider = "prisma-client"
output = "../generated"
moduleFormat = "esm"
provider = "prisma-client"
output = "../generated"
moduleFormat = "esm"
previewFeatures = ["relationJoins"]
}
datasource db {
@ -18,6 +19,7 @@ model bookmarks {
@@id([tmdb_id, user_id])
@@unique([tmdb_id, user_id], map: "bookmarks_tmdb_id_user_id_unique")
@@index([user_id], type: Hash)
}
model challenge_codes {
@ -72,6 +74,7 @@ model progress_items {
episode_number Int?
@@unique([tmdb_id, user_id, season_id, episode_id], map: "progress_items_tmdb_id_user_id_season_id_episode_id_unique")
@@index([user_id], type: Hash)
}
model sessions {
@ -82,6 +85,8 @@ model sessions {
expires_at DateTime @db.Timestamptz(0)
device String
user_agent String
@@index([user], type: Hash)
}
model user_group_order {
@ -158,4 +163,5 @@ model watch_history {
updated_at DateTime @default(now()) @db.Timestamptz(0)
@@unique([tmdb_id, user_id, season_id, episode_id], map: "watch_history_tmdb_id_user_id_season_id_episode_id_unique")
@@index([user_id, watched_at(sort: Desc)])
}

View file

@ -18,6 +18,7 @@ export default defineEventHandler(async event => {
const user = await prisma.users.findUnique({
where: { public_key: body.publicKey },
select: { id: true },
});
if (!user) {

View file

@ -1,7 +1,7 @@
import { z } from 'zod';
import { useChallenge } from '~/utils/challenge';
import { useAuth } from '~/utils/auth';
import { randomUUID } from 'crypto';
import { uuidv7 } from 'uuidv7';
import { generateRandomNickname } from '~/utils/nickname';
const completeSchema = z.object({
@ -50,7 +50,7 @@ export default defineEventHandler(async event => {
});
}
const userId = randomUUID();
const userId = uuidv7();
const now = new Date();
const nickname = generateRandomNickname();

View file

@ -5,6 +5,14 @@ export default defineEventHandler(async event => {
const user = await prisma.users.findUnique({
where: { id: session.user },
select: {
id: true,
public_key: true,
namespace: true,
nickname: true,
profile: true,
permissions: true,
},
});
if (!user) {

View file

@ -1,6 +1,5 @@
import { useAuth } from '~/utils/auth';
import { z } from 'zod';
import { bookmarks } from '@prisma/client';
const bookmarkMetaSchema = z.object({
title: z.string(),
@ -32,9 +31,16 @@ export default defineEventHandler(async event => {
if (method === 'GET') {
const bookmarks = await prisma.bookmarks.findMany({
where: { user_id: userId },
select: {
tmdb_id: true,
meta: true,
group: true,
favorite_episodes: true,
updated_at: true,
},
});
return bookmarks.map((bookmark: bookmarks) => ({
return bookmarks.map((bookmark: any) => ({
tmdbId: bookmark.tmdb_id,
meta: bookmark.meta,
group: bookmark.group,
@ -48,18 +54,16 @@ export default defineEventHandler(async event => {
const validatedBody = z.array(bookmarkDataSchema).parse(body);
const now = new Date();
const results = [];
for (const item of validatedBody) {
const upserts = validatedBody.map((item: any) => {
// Normalize group to always be an array
const normalizedGroup = item.group
const normalizedGroup = item.group
? (Array.isArray(item.group) ? item.group : [item.group])
: [];
// Normalize favoriteEpisodes to always be an array
const normalizedFavoriteEpisodes = item.favoriteEpisodes || [];
const bookmark = await prisma.bookmarks.upsert({
return prisma.bookmarks.upsert({
where: {
tmdb_id_user_id: {
tmdb_id: item.tmdbId,
@ -80,18 +84,18 @@ export default defineEventHandler(async event => {
favorite_episodes: normalizedFavoriteEpisodes,
updated_at: now,
} as any,
}) as bookmarks;
results.push({
tmdbId: bookmark.tmdb_id,
meta: bookmark.meta,
group: bookmark.group,
favoriteEpisodes: bookmark.favorite_episodes,
updatedAt: bookmark.updated_at,
});
}
});
return results;
const bookmarks = await prisma.$transaction(upserts);
return bookmarks.map((bookmark: any) => ({
tmdbId: bookmark.tmdb_id,
meta: bookmark.meta,
group: bookmark.group,
favoriteEpisodes: bookmark.favorite_episodes,
updatedAt: bookmark.updated_at,
}));
}

View file

@ -1,4 +1,4 @@
import { randomUUID } from 'crypto';
import { uuidv7 } from 'uuidv7';
import { useAuth } from '~/utils/auth';
import { z } from 'zod';
@ -38,7 +38,7 @@ export default defineEventHandler(async event => {
updated_at: new Date(),
},
create: {
id: randomUUID(),
id: uuidv7(),
user_id: userId,
group_order: validatedGroupOrder,
},

View file

@ -94,11 +94,35 @@ export default defineEventHandler(async event => {
where: { user_id: userId },
});
await tx.watch_history.deleteMany({
where: { user_id: userId },
});
const userLists = await tx.lists.findMany({
where: { user_id: userId },
select: { id: true }
});
const listIds = userLists.map((l: any) => l.id);
if (listIds.length > 0) {
await tx.list_items.deleteMany({
where: { list_id: { in: listIds } },
});
}
await tx.lists.deleteMany({
where: { user_id: userId },
});
await tx.user_group_order.deleteMany({
where: { user_id: userId },
});
await tx.user_settings
.delete({
where: { id: userId },
})
.catch(() => {});
.catch(() => { });
await tx.sessions.deleteMany({
where: { user: userId },

View file

@ -1,6 +1,7 @@
import { useAuth } from '#imports';
import { z } from 'zod';
import { prisma } from '#imports';
import { uuidv7 } from 'uuidv7';
const listItemSchema = z.object({
tmdb_id: z.string(),
@ -62,6 +63,7 @@ export default defineEventHandler(async event => {
description:
validatedBody.description !== undefined ? validatedBody.description : list.description,
public: validatedBody.public ?? list.public,
updated_at: new Date(),
},
});
}
@ -76,6 +78,7 @@ export default defineEventHandler(async event => {
if (itemsToAdd.length > 0) {
await tx.list_items.createMany({
data: itemsToAdd.map(item => ({
id: uuidv7(),
list_id: list.id,
tmdb_id: item.tmdb_id,
type: item.type,

View file

@ -1,6 +1,7 @@
import { useAuth } from '#imports';
import { prisma } from '~/utils/prisma';
import { z } from 'zod';
import { uuidv7 } from 'uuidv7';
const listItemSchema = z.object({
tmdb_id: z.string(),
@ -40,18 +41,30 @@ export default defineEventHandler(async event => {
const validatedBody = createListSchema.parse(parsedBody);
const result = await prisma.$transaction(async tx => {
const existing = await tx.lists.findFirst({
where: { user_id: userId, name: validatedBody.name }
});
if (existing) {
throw createError({ statusCode: 409, message: 'A list with this name already exists' });
}
const now = new Date();
const newList = await tx.lists.create({
data: {
id: uuidv7(),
user_id: userId,
name: validatedBody.name,
description: validatedBody.description || null,
public: validatedBody.public || false,
updated_at: now,
},
});
if (validatedBody.items && validatedBody.items.length > 0) {
await tx.list_items.createMany({
data: validatedBody.items.map(item => ({
id: uuidv7(),
list_id: newList.id,
tmdb_id: item.tmdb_id,
type: item.type, // Type is mapped here

View file

@ -1,6 +1,6 @@
import { useAuth } from '~/utils/auth';
import { z } from 'zod';
import { randomUUID } from 'crypto';
import { uuidv7 } from 'uuidv7';
function progressIsNotStarted(duration: number, watched: number): boolean {
// too short watch time
@ -56,7 +56,7 @@ async function shouldSaveProgress(
const epDuration = Number(episode.duration);
const epWatched = Number(episode.watched);
return !progressIsNotStarted(epDuration, epWatched) &&
!progressIsCompleted(epDuration, epWatched);
!progressIsCompleted(epDuration, epWatched);
});
}
@ -110,6 +110,18 @@ export default defineEventHandler(async event => {
if (method === 'GET') {
const items = await prisma.progress_items.findMany({
where: { user_id: userId },
select: {
id: true,
tmdb_id: true,
episode_id: true,
episode_number: true,
season_id: true,
season_number: true,
meta: true,
duration: true,
watched: true,
updated_at: true,
},
});
return items.map(item => ({
@ -179,7 +191,7 @@ export default defineEventHandler(async event => {
const duration = Number(item.duration);
const watched = Number(item.watched);
return !progressIsNotStarted(duration, watched) &&
!progressIsCompleted(duration, watched);
!progressIsCompleted(duration, watched);
});
if (hasAcceptableEpisodes) {
@ -245,7 +257,7 @@ export default defineEventHandler(async event => {
const now = defaultAndCoerceDateTime(validatedBody.updatedAt);
const existingItem = await prisma.progress_items.findUnique({
const progressItem = await prisma.progress_items.upsert({
where: {
tmdb_id_user_id_season_id_episode_id: {
tmdb_id: tmdbId,
@ -254,40 +266,27 @@ export default defineEventHandler(async event => {
episode_id: validatedBody.episodeId || null,
},
},
update: {
duration: BigInt(validatedBody.duration),
watched: BigInt(validatedBody.watched),
meta: validatedBody.meta,
updated_at: now,
},
create: {
id: uuidv7(),
tmdb_id: tmdbId,
user_id: userId,
season_id: validatedBody.seasonId || null,
episode_id: validatedBody.episodeId || null,
season_number: validatedBody.seasonNumber || null,
episode_number: validatedBody.episodeNumber || null,
duration: BigInt(validatedBody.duration),
watched: BigInt(validatedBody.watched),
meta: validatedBody.meta,
updated_at: now,
},
});
let progressItem;
if (existingItem) {
progressItem = await prisma.progress_items.update({
where: {
id: existingItem.id,
},
data: {
duration: BigInt(validatedBody.duration),
watched: BigInt(validatedBody.watched),
meta: validatedBody.meta,
updated_at: now,
},
});
} else {
progressItem = await prisma.progress_items.create({
data: {
id: randomUUID(),
tmdb_id: tmdbId,
user_id: userId,
season_id: validatedBody.seasonId || null,
episode_id: validatedBody.episodeId || null,
season_number: validatedBody.seasonNumber || null,
episode_number: validatedBody.episodeNumber || null,
duration: BigInt(validatedBody.duration),
watched: BigInt(validatedBody.watched),
meta: validatedBody.meta,
updated_at: now,
},
});
}
return {
id: progressItem.id,
tmdbId: progressItem.tmdb_id,
@ -314,25 +313,12 @@ export default defineEventHandler(async event => {
if (body.seasonId) whereClause.season_id = body.seasonId;
if (body.episodeId) whereClause.episode_id = body.episodeId;
const itemsToDelete = await prisma.progress_items.findMany({
where: whereClause,
});
if (itemsToDelete.length === 0) {
return {
count: 0,
tmdbId,
episodeId: body.episodeId,
seasonId: body.seasonId,
};
}
await prisma.progress_items.deleteMany({
const { count } = await prisma.progress_items.deleteMany({
where: whereClause,
});
return {
count: itemsToDelete.length,
count,
tmdbId,
episodeId: body.episodeId,
seasonId: body.seasonId,

View file

@ -1,6 +1,6 @@
import { useAuth } from '~/utils/auth';
import { z } from 'zod';
import { randomUUID } from 'crypto';
import { uuidv7 } from 'uuidv7';
const progressMetaSchema = z.object({
title: z.string(),
@ -70,10 +70,6 @@ export default defineEventHandler(async (event) => {
const now = coerceDateTime(updatedAt);
const { seasonId: normSeasonId, episodeId: normEpisodeId } = normalizeIds(meta.type, seasonId, episodeId);
const existing = await prisma.progress_items.findUnique({
where: { tmdb_id_user_id_season_id_episode_id: { tmdb_id: tmdbId, user_id: userId, season_id: normSeasonId, episode_id: normEpisodeId } },
});
const data = {
duration: BigInt(duration),
watched: BigInt(watched),
@ -81,20 +77,20 @@ export default defineEventHandler(async (event) => {
updated_at: now,
};
const progressItem = existing
? await prisma.progress_items.update({ where: { id: existing.id }, data })
: await prisma.progress_items.create({
data: {
id: randomUUID(),
tmdb_id: tmdbId,
user_id: userId,
season_id: normSeasonId,
episode_id: normEpisodeId,
season_number: seasonNumber || null,
episode_number: episodeNumber || null,
...data,
},
});
const progressItem = await prisma.progress_items.upsert({
where: { tmdb_id_user_id_season_id_episode_id: { tmdb_id: tmdbId, user_id: userId, season_id: normSeasonId, episode_id: normEpisodeId } },
update: data,
create: {
id: uuidv7(),
tmdb_id: tmdbId,
user_id: userId,
season_id: normSeasonId,
episode_id: normEpisodeId,
season_number: seasonNumber || null,
episode_number: episodeNumber || null,
...data,
},
});
return formatProgressItem(progressItem);
}
@ -109,11 +105,8 @@ export default defineEventHandler(async (event) => {
if (body.episodeId) where.episode_id = body.episodeId;
else if (body.meta?.type === 'movie') where.episode_id = '\n';
const items = await prisma.progress_items.findMany({ where });
if (items.length === 0) return { count: 0, tmdbId, episodeId: body.episodeId, seasonId: body.seasonId };
await prisma.progress_items.deleteMany({ where });
return { count: items.length, tmdbId, episodeId: body.episodeId, seasonId: body.seasonId };
const { count } = await prisma.progress_items.deleteMany({ where });
return { count, tmdbId, episodeId: body.episodeId, seasonId: body.seasonId };
}
throw createError({ statusCode: 405, message: 'Method not allowed' });

View file

@ -1,6 +1,6 @@
import { useAuth } from '~/utils/auth';
import { z } from 'zod';
import { randomUUID } from 'crypto';
import { uuidv7 } from 'uuidv7';
import { scopedLogger } from '~/utils/logger';
const log = scopedLogger('progress-import');
@ -14,7 +14,7 @@ const progressMetaSchema = z.object({
const progressItemSchema = z.object({
meta: progressMetaSchema,
tmdbId: z.string().transform(val => val || randomUUID()),
tmdbId: z.string().transform(val => val || uuidv7()),
duration: z
.number()
.min(0)
@ -54,6 +54,7 @@ export default defineEventHandler(async event => {
// First check if user exists
const user = await prisma.users.findUnique({
where: { id: userId },
select: { id: true }
});
if (!user) {
@ -117,7 +118,7 @@ export default defineEventHandler(async event => {
for (const item of newItems) {
const isMovie = item.meta.type === 'movie';
itemsToUpsert.push({
id: randomUUID(),
id: uuidv7(),
tmdb_id: item.tmdbId,
user_id: userId,
season_id: isMovie ? '\n' : item.seasonId || null,
@ -132,54 +133,54 @@ export default defineEventHandler(async event => {
}
// Upsert all items
const results = [];
for (const item of itemsToUpsert) {
try {
const result = await prisma.progress_items.upsert({
where: {
tmdb_id_user_id_season_id_episode_id: {
tmdb_id: item.tmdb_id,
user_id: item.user_id,
season_id: item.season_id,
episode_id: item.episode_id,
},
const upsertPromises = itemsToUpsert.map(item =>
prisma.progress_items.upsert({
where: {
tmdb_id_user_id_season_id_episode_id: {
tmdb_id: item.tmdb_id,
user_id: item.user_id,
season_id: item.season_id,
episode_id: item.episode_id,
},
create: item,
update: {
duration: item.duration,
watched: item.watched,
meta: item.meta,
updated_at: item.updated_at,
},
});
},
create: item,
update: {
duration: item.duration,
watched: item.watched,
meta: item.meta,
updated_at: item.updated_at,
},
})
);
results.push({
id: result.id,
tmdbId: result.tmdb_id,
episode: {
id: result.episode_id === '\n' ? null : result.episode_id,
number: result.episode_number,
},
season: {
id: result.season_id === '\n' ? null : result.season_id,
number: result.season_number,
},
meta: result.meta,
duration: result.duration.toString(),
watched: result.watched.toString(),
updatedAt: result.updated_at.toISOString(),
});
} catch (error) {
log.error('Failed to upsert progress item', {
userId,
tmdbId: item.tmdb_id,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
try {
const transactionResults = await prisma.$transaction(upsertPromises);
const results = transactionResults.map(result => ({
id: result.id,
tmdbId: result.tmdb_id,
episode: {
id: result.episode_id === '\n' ? null : result.episode_id,
number: result.episode_number,
},
season: {
id: result.season_id === '\n' ? null : result.season_id,
number: result.season_number,
},
meta: result.meta,
duration: result.duration.toString(),
watched: result.watched.toString(),
updatedAt: result.updated_at.toISOString(),
}));
return results;
} catch (error) {
log.error('Failed to batch upsert progress items', {
userId,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
return results;
} catch (error) {
log.error('Failed to import progress', {
userId,

View file

@ -14,6 +14,14 @@ export default defineEventHandler(async event => {
const sessions = await prisma.sessions.findMany({
where: { user: userId },
select: {
id: true,
user: true,
created_at: true,
accessed_at: true,
device: true,
user_agent: true,
}
});
return sessions.map(s => ({

View file

@ -61,6 +61,7 @@ export default defineEventHandler(async event => {
// First check if user exists
const user = await prisma.users.findUnique({
where: { id: userId },
select: { id: true },
});
if (!user) {

View file

@ -1,6 +1,6 @@
import { useAuth } from '~/utils/auth';
import { z } from 'zod';
import { randomUUID } from 'crypto';
import { uuidv7 } from 'uuidv7';
const watchHistoryMetaSchema = z.object({
title: z.string(),
@ -54,6 +54,18 @@ export default defineEventHandler(async event => {
const items = await prisma.watch_history.findMany({
where: { user_id: userId },
orderBy: { watched_at: 'desc' },
select: {
tmdb_id: true,
episode_id: true,
episode_number: true,
season_id: true,
season_number: true,
meta: true,
duration: true,
watched: true,
watched_at: true,
completed: true,
}
});
return items.map(item => ({

View file

@ -1,6 +1,6 @@
import { useAuth } from '~/utils/auth';
import { z } from 'zod';
import { randomUUID } from 'crypto';
import { uuidv7 } from 'uuidv7';
const watchHistoryMetaSchema = z.object({
title: z.string(),
@ -63,9 +63,7 @@ export default defineEventHandler(async event => {
const parsed = bodySchema.parse(body);
const items = Array.isArray(parsed) ? parsed : [parsed];
const results = [];
for (const validatedBody of items) {
const upsertPromises = items.map(validatedBody => {
const itemTmdbId = items.length === 1 ? tmdbId : (validatedBody.tmdbId ?? tmdbId);
const watchedAt = defaultAndCoerceDateTime(validatedBody.watchedAt);
const now = new Date();
@ -74,17 +72,6 @@ export default defineEventHandler(async event => {
const normSeasonId = validatedBody.meta.type === 'movie' ? '\n' : validatedBody.seasonId ?? null;
const normEpisodeId = validatedBody.meta.type === 'movie' ? '\n' : validatedBody.episodeId ?? null;
const existingItem = await prisma.watch_history.findUnique({
where: {
tmdb_id_user_id_season_id_episode_id: {
tmdb_id: itemTmdbId,
user_id: userId,
season_id: normSeasonId,
episode_id: normEpisodeId,
},
},
});
const data = {
duration: parseFloat(validatedBody.duration),
watched: parseFloat(validatedBody.watched),
@ -94,45 +81,47 @@ export default defineEventHandler(async event => {
updated_at: now,
};
let watchHistoryItem;
if (existingItem) {
watchHistoryItem = await prisma.watch_history.update({
where: { id: existingItem.id },
data,
});
} else {
watchHistoryItem = await prisma.watch_history.create({
data: {
id: randomUUID(),
return prisma.watch_history.upsert({
where: {
tmdb_id_user_id_season_id_episode_id: {
tmdb_id: itemTmdbId,
user_id: userId,
season_id: normSeasonId,
episode_id: normEpisodeId,
season_number: validatedBody.seasonNumber ?? null,
episode_number: validatedBody.episodeNumber ?? null,
...data,
},
});
}
results.push({
success: true,
id: watchHistoryItem.id,
tmdbId: watchHistoryItem.tmdb_id,
userId: watchHistoryItem.user_id,
seasonId: watchHistoryItem.season_id,
episodeId: watchHistoryItem.episode_id,
seasonNumber: watchHistoryItem.season_number,
episodeNumber: watchHistoryItem.episode_number,
meta: watchHistoryItem.meta,
duration: watchHistoryItem.duration,
watched: watchHistoryItem.watched,
watchedAt: watchHistoryItem.watched_at.toISOString(),
completed: watchHistoryItem.completed,
updatedAt: watchHistoryItem.updated_at.toISOString(),
},
update: data,
create: {
id: uuidv7(),
tmdb_id: itemTmdbId,
user_id: userId,
season_id: normSeasonId,
episode_id: normEpisodeId,
season_number: validatedBody.seasonNumber ?? null,
episode_number: validatedBody.episodeNumber ?? null,
...data,
},
});
}
});
const transactionResults = await prisma.$transaction(upsertPromises);
const results = transactionResults.map(watchHistoryItem => ({
success: true,
id: watchHistoryItem.id,
tmdbId: watchHistoryItem.tmdb_id,
userId: watchHistoryItem.user_id,
seasonId: watchHistoryItem.season_id,
episodeId: watchHistoryItem.episode_id,
seasonNumber: watchHistoryItem.season_number,
episodeNumber: watchHistoryItem.episode_number,
meta: watchHistoryItem.meta,
duration: watchHistoryItem.duration,
watched: watchHistoryItem.watched,
watchedAt: watchHistoryItem.watched_at.toISOString(),
completed: watchHistoryItem.completed,
updatedAt: watchHistoryItem.updated_at.toISOString(),
}));
return results.length === 1 ? results[0] : { success: true, count: results.length, items: results };
} catch (dbError) {

View file

@ -1,7 +1,7 @@
import { prisma } from './prisma';
import jwt from 'jsonwebtoken';
const { sign, verify } = jwt;
import { randomUUID } from 'crypto';
import { uuidv7 } from 'uuidv7';
// 21 days in ms
const SESSION_EXPIRY_MS = 21 * 24 * 60 * 60 * 1000;
@ -40,9 +40,14 @@ export function useAuth() {
const now = new Date();
const expiryDate = new Date(now.getTime() + SESSION_EXPIRY_MS);
// Cleanup existing session for this device
await prisma.sessions.deleteMany({
where: { user, device },
});
return await prisma.sessions.create({
data: {
id: randomUUID(),
id: uuidv7(),
user,
device,
user_agent: userAgent,
@ -56,7 +61,7 @@ export function useAuth() {
const makeSessionToken = (session: { id: string }) => {
const runtimeConfig = useRuntimeConfig();
const cryptoSecret = runtimeConfig.cryptoSecret || process.env.CRYPTO_SECRET;
if (!cryptoSecret) {
console.error('CRYPTO_SECRET is missing from both runtime config and environment');
console.error('Available runtime config keys:', Object.keys(runtimeConfig));
@ -66,7 +71,7 @@ export function useAuth() {
});
throw new Error('CRYPTO_SECRET environment variable is not set');
}
return sign({ sid: session.id }, cryptoSecret, {
algorithm: 'HS256',
});
@ -76,12 +81,12 @@ export function useAuth() {
try {
const runtimeConfig = useRuntimeConfig();
const cryptoSecret = runtimeConfig.cryptoSecret || process.env.CRYPTO_SECRET;
if (!cryptoSecret) {
console.error('CRYPTO_SECRET is missing for token verification');
return null;
}
const payload = verify(token, cryptoSecret, {
algorithms: ['HS256'],
});

View file

@ -1,4 +1,4 @@
import { randomUUID } from 'crypto';
import { uuidv7 } from 'uuidv7';
import { prisma } from './prisma';
import nacl from 'tweetnacl';
@ -12,7 +12,7 @@ export function useChallenge() {
return await prisma.challenge_codes.create({
data: {
code: randomUUID(),
code: uuidv7(),
flow,
auth_type: authType,
created_at: now,

View file

@ -28,7 +28,7 @@ export interface PlayerStatus {
// In-memory store for player status data
// Key: userId+roomCode, Value: Status data array
export const playerStatusStore = new Map<string, Array<PlayerStatus>>();
export const playerStatusStore = new Map<string, PlayerStatus[]>();
// Cleanup interval (30 minutes in milliseconds)
export const CLEANUP_INTERVAL = 30 * 60 * 1000;

View file

@ -1,14 +1,20 @@
import { Pool } from 'pg';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../../generated/client';
const adapter = new PrismaPg({
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: parseInt(process.env.DB_POOL_MAX || '100000', 10),
connectionTimeoutMillis: 10000,
idleTimeoutMillis: 300000,
});
const adapter = new PrismaPg(pool);
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = new PrismaClient({ adapter });
export const prisma = globalForPrisma.prisma || new PrismaClient({ adapter });
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;