From 96e9d832e5e08afb8f460f4d7557556706b6f3e7 Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Thu, 13 Mar 2025 00:00:28 -0600 Subject: [PATCH] Reapply with fixes I think I got it this time --- .gitignore | 1 + server/routes/metrics/providers.post.ts | 9 + server/routes/metrics/providers.put.ts | 2 + server/routes/users/[id]/bookmarks.ts | 5 +- .../users/[id]/bookmarks/[tmdbid]/index.ts | 123 +++++++--- server/routes/users/[id]/index.ts | 78 ++++++ server/routes/users/[id]/progress.ts | 29 ++- server/routes/users/[id]/progress/import.ts | 222 +++++++++++------- server/routes/users/[id]/sessions.ts | 10 +- server/routes/users/[id]/settings.ts | 156 ++++++------ 10 files changed, 421 insertions(+), 214 deletions(-) create mode 100644 server/routes/metrics/providers.put.ts create mode 100644 server/routes/users/[id]/index.ts diff --git a/.gitignore b/.gitignore index a9d9387..f4c22ec 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist .env .vscode .metrics.json +.metrics.json diff --git a/server/routes/metrics/providers.post.ts b/server/routes/metrics/providers.post.ts index cacc2d0..22c15f1 100644 --- a/server/routes/metrics/providers.post.ts +++ b/server/routes/metrics/providers.post.ts @@ -32,9 +32,18 @@ const metricsProviderSchema = z.object({ const metricsProviderInputSchema = z.object({ items: z.array(metricsProviderSchema).max(10).min(1), tool: z.string().optional(), + batchId: z.string().optional(), }); export default defineEventHandler(async (event) => { + // Handle both POST and PUT methods + if (event.method !== 'POST' && event.method !== 'PUT') { + throw createError({ + statusCode: 405, + message: 'Method not allowed' + }); + } + try { await ensureMetricsInitialized(); diff --git a/server/routes/metrics/providers.put.ts b/server/routes/metrics/providers.put.ts new file mode 100644 index 0000000..4726675 --- /dev/null +++ b/server/routes/metrics/providers.put.ts @@ -0,0 +1,2 @@ +// Redirect to the POST handler which now supports both methods +export { default } from './providers.post'; \ No newline at end of file diff --git a/server/routes/users/[id]/bookmarks.ts b/server/routes/users/[id]/bookmarks.ts index 5e83c0a..d099173 100644 --- a/server/routes/users/[id]/bookmarks.ts +++ b/server/routes/users/[id]/bookmarks.ts @@ -5,7 +5,7 @@ const bookmarkMetaSchema = z.object({ title: z.string(), year: z.number().optional(), poster: z.string().optional(), - type: z.enum(['movie', 'tv']), + type: z.enum(['movie', 'show']), }); const bookmarkDataSchema = z.object({ @@ -33,7 +33,6 @@ export default defineEventHandler(async (event) => { return bookmarks.map(bookmark => ({ tmdbId: bookmark.tmdb_id, - userId: bookmark.user_id, meta: bookmark.meta, updatedAt: bookmark.updated_at })); @@ -68,7 +67,6 @@ export default defineEventHandler(async (event) => { results.push({ tmdbId: bookmark.tmdb_id, - userId: bookmark.user_id, meta: bookmark.meta, updatedAt: bookmark.updated_at }); @@ -111,7 +109,6 @@ export default defineEventHandler(async (event) => { return { tmdbId: bookmark.tmdb_id, - userId: bookmark.user_id, meta: bookmark.meta, updatedAt: bookmark.updated_at }; diff --git a/server/routes/users/[id]/bookmarks/[tmdbid]/index.ts b/server/routes/users/[id]/bookmarks/[tmdbid]/index.ts index b721643..baa8f22 100644 --- a/server/routes/users/[id]/bookmarks/[tmdbid]/index.ts +++ b/server/routes/users/[id]/bookmarks/[tmdbid]/index.ts @@ -1,6 +1,25 @@ +import { useAuth } from '~/utils/auth'; +import { z } from 'zod'; +import { scopedLogger } from '~/utils/logger'; + +const log = scopedLogger('user-bookmarks'); + +const bookmarkMetaSchema = z.object({ + title: z.string(), + year: z.number(), + poster: z.string().optional(), + type: z.enum(['movie', 'show']) +}); + +// Support both formats: direct fields or nested under meta +const bookmarkRequestSchema = z.object({ + meta: bookmarkMetaSchema.optional(), + tmdbId: z.string().optional() +}); + export default defineEventHandler(async (event) => { - const userId = getRouterParam(event, 'id') - const tmdbId = getRouterParam(event, 'tmdbid') + const userId = getRouterParam(event, 'id'); + const tmdbId = getRouterParam(event, 'tmdbid'); const session = await useAuth().getCurrentSession(); @@ -12,37 +31,81 @@ export default defineEventHandler(async (event) => { } if (event.method === "POST") { - const body = await readBody(event); - const bookmark = await prisma.bookmarks.create({ - data: { - user_id: session.user, - tmdb_id: tmdbId, - meta: body.meta, - updated_at: new Date() - } - }); - - return { - tmdbId: bookmark.tmdb_id, - userId: bookmark.user_id, - meta: bookmark.meta, - updatedAt: bookmark.updated_at - }; -} else if (event.method === "DELETE") { - await prisma.bookmarks.delete({ - where: { - tmdb_id_user_id: { - tmdb_id: tmdbId, - user_id: session.user + try { + const body = await readBody(event); + log.info('Creating bookmark', { userId, tmdbId, body }); + + // Parse and validate the request body + const validatedRequest = bookmarkRequestSchema.parse(body); + + // Extract the meta data - either directly from meta field or from the root + const metaData = validatedRequest.meta || body; + + // Validate the meta data separately + const validatedMeta = bookmarkMetaSchema.parse(metaData); + + const bookmark = await prisma.bookmarks.create({ + data: { + user_id: session.user, + tmdb_id: tmdbId, + meta: validatedMeta, + updated_at: new Date() + } + }); + + log.info('Bookmark created successfully', { userId, tmdbId }); + + return { + tmdbId: bookmark.tmdb_id, + meta: bookmark.meta, + updatedAt: bookmark.updated_at + }; + } catch (error) { + log.error('Failed to create bookmark', { + userId, + tmdbId, + error: error instanceof Error ? error.message : String(error) + }); + + if (error instanceof z.ZodError) { + throw createError({ + statusCode: 400, + message: JSON.stringify(error.errors, null, 2) + }); } + + throw error; } - }); - - return { success: true, tmdbId }; -} + } else if (event.method === "DELETE") { + log.info('Deleting bookmark', { userId, tmdbId }); + + try { + await prisma.bookmarks.delete({ + where: { + tmdb_id_user_id: { + tmdb_id: tmdbId, + user_id: session.user + } + } + }); + + log.info('Bookmark deleted successfully', { userId, tmdbId }); + + return { success: true, tmdbId }; + } catch (error) { + log.error('Failed to delete bookmark', { + userId, + tmdbId, + error: error instanceof Error ? error.message : String(error) + }); + + // If bookmark doesn't exist, still return success + return { success: true, tmdbId }; + } + } throw createError({ statusCode: 405, message: 'Method not allowed' - }) -}) \ No newline at end of file + }); +}); \ No newline at end of file diff --git a/server/routes/users/[id]/index.ts b/server/routes/users/[id]/index.ts new file mode 100644 index 0000000..765cea2 --- /dev/null +++ b/server/routes/users/[id]/index.ts @@ -0,0 +1,78 @@ +import { useAuth } from '~/utils/auth'; +import { z } from 'zod'; +import { scopedLogger } from '~/utils/logger'; + +const log = scopedLogger('user-profile'); + +const userProfileSchema = z.object({ + profile: z.object({ + icon: z.string(), + colorA: z.string(), + colorB: z.string() + }) +}); + +export default defineEventHandler(async (event) => { + const userId = event.context.params?.id; + + const session = await useAuth().getCurrentSession(); + + if (session.user !== userId) { + throw createError({ + statusCode: 403, + message: 'Cannot modify other users' + }); + } + + if (event.method === 'PATCH') { + try { + const body = await readBody(event); + log.info('Updating user profile', { userId, body }); + + const validatedBody = userProfileSchema.parse(body); + + const user = await prisma.users.update({ + where: { id: userId }, + data: { + profile: validatedBody.profile + } + }); + + log.info('User profile updated successfully', { userId }); + + return { + id: user.id, + publicKey: user.public_key, + namespace: user.namespace, + profile: user.profile, + permissions: user.permissions, + createdAt: user.created_at, + lastLoggedIn: user.last_logged_in + }; + } catch (error) { + log.error('Failed to update user profile', { + userId, + error: error instanceof Error ? error.message : String(error) + }); + + if (error instanceof z.ZodError) { + throw createError({ + statusCode: 400, + message: 'Invalid profile data', + cause: error.errors + }); + } + + throw createError({ + statusCode: 500, + message: 'Failed to update user profile', + cause: error instanceof Error ? error.message : 'Unknown error' + }); + } + } + + throw createError({ + statusCode: 405, + message: 'Method not allowed' + }); +}); \ No newline at end of file diff --git a/server/routes/users/[id]/progress.ts b/server/routes/users/[id]/progress.ts index 6783164..b0111d5 100644 --- a/server/routes/users/[id]/progress.ts +++ b/server/routes/users/[id]/progress.ts @@ -4,16 +4,16 @@ import { randomUUID } from 'crypto'; const progressMetaSchema = z.object({ title: z.string(), + year: z.number().optional(), poster: z.string().optional(), - type: z.enum(['movie', 'tv', 'show']), - year: z.number().optional() + type: z.enum(['movie', 'show']) }); const progressItemSchema = z.object({ meta: progressMetaSchema, tmdbId: z.string(), - duration: z.number().transform((n) => Math.round(n)), - watched: z.number().transform((n) => Math.round(n)), + duration: z.number().transform((n) => n.toString()), + watched: z.number().transform((n) => n.toString()), seasonId: z.string().optional(), episodeId: z.string().optional(), seasonNumber: z.number().optional(), @@ -45,7 +45,7 @@ export default defineEventHandler(async (event) => { if (session.user !== userId) { throw createError({ statusCode: 403, - message: 'Cannot modify user other than yourself' + message: 'Cannot access other user information' }); } @@ -57,15 +57,18 @@ export default defineEventHandler(async (event) => { return items.map(item => ({ id: item.id, tmdbId: item.tmdb_id, - userId: item.user_id, - seasonId: item.season_id, - episodeId: item.episode_id, - seasonNumber: item.season_number, - episodeNumber: item.episode_number, + episode: { + id: item.episode_id || null, + number: item.episode_number || null + }, + season: { + id: item.season_id || null, + number: item.season_number || null + }, meta: item.meta, - duration: Number(item.duration), - watched: Number(item.watched), - updatedAt: item.updated_at + duration: item.duration.toString(), + watched: item.watched.toString(), + updatedAt: item.updated_at.toISOString() })); } diff --git a/server/routes/users/[id]/progress/import.ts b/server/routes/users/[id]/progress/import.ts index d55cbff..ec0309f 100644 --- a/server/routes/users/[id]/progress/import.ts +++ b/server/routes/users/[id]/progress/import.ts @@ -1,24 +1,27 @@ import { useAuth } from '~/utils/auth'; import { z } from 'zod'; import { randomUUID } from 'crypto'; +import { scopedLogger } from '~/utils/logger'; + +const log = scopedLogger('progress-import'); const progressMetaSchema = z.object({ title: z.string(), - poster: z.string().optional(), - type: z.enum(['movie', 'tv', 'show']), - year: z.number().optional() + type: z.enum(['movie', 'show']), + year: z.number(), + poster: z.string().optional() }); const progressItemSchema = z.object({ meta: progressMetaSchema, tmdbId: z.string(), - duration: z.number().transform((n) => Math.round(n)), - watched: z.number().transform((n) => Math.round(n)), + duration: z.number(), + watched: z.number(), seasonId: z.string().optional(), episodeId: z.string().optional(), seasonNumber: z.number().optional(), episodeNumber: z.number().optional(), - updatedAt: z.string().datetime({ offset: true }).optional(), + updatedAt: z.string().datetime({ offset: true }).optional() }); // 13th July 2021 - movie-web epoch @@ -49,91 +52,130 @@ export default defineEventHandler(async (event) => { }); } - const body = await readBody(event); - const validatedBody = z.array(progressItemSchema).parse(body); - - const existingItems = await prisma.progress_items.findMany({ - where: { user_id: userId } - }); - - const newItems = [...validatedBody]; - const itemsToUpsert = []; - - for (const existingItem of existingItems) { - const newItemIndex = newItems.findIndex( - (item) => - item.tmdbId === existingItem.tmdb_id && - item.seasonId === existingItem.season_id && - item.episodeId === existingItem.episode_id - ); + try { + const body = await readBody(event); + const validatedBody = z.array(progressItemSchema).parse(body); - if (newItemIndex > -1) { - const newItem = newItems[newItemIndex]; - - if (Number(existingItem.watched) < newItem.watched) { - itemsToUpsert.push({ - id: existingItem.id, - tmdb_id: existingItem.tmdb_id, - user_id: existingItem.user_id, - season_id: existingItem.season_id, - episode_id: existingItem.episode_id, - season_number: existingItem.season_number, - episode_number: existingItem.episode_number, - duration: BigInt(newItem.duration), - watched: BigInt(newItem.watched), - meta: newItem.meta, - updated_at: defaultAndCoerceDateTime(newItem.updatedAt) - }); - } - - newItems.splice(newItemIndex, 1); - } - } - - for (const newItem of newItems) { - itemsToUpsert.push({ - id: randomUUID(), - tmdb_id: newItem.tmdbId, - user_id: userId, - season_id: newItem.seasonId || null, - episode_id: newItem.episodeId || null, - season_number: newItem.seasonNumber || null, - episode_number: newItem.episodeNumber || null, - duration: BigInt(newItem.duration), - watched: BigInt(newItem.watched), - meta: newItem.meta, - updated_at: defaultAndCoerceDateTime(newItem.updatedAt) + const existingItems = await prisma.progress_items.findMany({ + where: { user_id: userId } }); + + const newItems = [...validatedBody]; + const itemsToUpsert = []; + + for (const existingItem of existingItems) { + const newItemIndex = newItems.findIndex( + (item) => + item.tmdbId === existingItem.tmdb_id && + item.seasonId === (existingItem.season_id === '\n' ? null : existingItem.season_id) && + item.episodeId === (existingItem.episode_id === '\n' ? null : existingItem.episode_id) + ); + + if (newItemIndex > -1) { + const newItem = newItems[newItemIndex]; + + if (Number(existingItem.watched) < newItem.watched) { + const isMovie = newItem.meta.type === 'movie'; + itemsToUpsert.push({ + id: existingItem.id, + tmdb_id: existingItem.tmdb_id, + user_id: existingItem.user_id, + season_id: isMovie ? '\n' : existingItem.season_id, + episode_id: isMovie ? '\n' : existingItem.episode_id, + season_number: existingItem.season_number, + episode_number: existingItem.episode_number, + duration: BigInt(newItem.duration), + watched: BigInt(newItem.watched), + meta: newItem.meta, + updated_at: defaultAndCoerceDateTime(newItem.updatedAt) + }); + } + + newItems.splice(newItemIndex, 1); + } + } + + // Create new items + for (const item of newItems) { + const isMovie = item.meta.type === 'movie'; + itemsToUpsert.push({ + id: randomUUID(), + tmdb_id: item.tmdbId, + user_id: userId, + season_id: isMovie ? '\n' : (item.seasonId || null), + episode_id: isMovie ? '\n' : (item.episodeId || null), + season_number: isMovie ? null : item.seasonNumber, + episode_number: isMovie ? null : item.episodeNumber, + duration: BigInt(item.duration), + watched: BigInt(item.watched), + meta: item.meta, + updated_at: defaultAndCoerceDateTime(item.updatedAt) + }); + } + + // 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 + } + }, + 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; + } + } + + return results; + } catch (error) { + log.error('Failed to import progress', { + userId, + error: error instanceof Error ? error.message : String(error) + }); + + if (error instanceof z.ZodError) { + throw createError({ + statusCode: 400, + message: 'Invalid progress data', + cause: error.errors + }); + } + + throw error; } - - const result = await prisma.$transaction( - itemsToUpsert.map(item => - prisma.progress_items.upsert({ - where: { - id: item.id - }, - update: { - watched: item.watched, - duration: item.duration, - meta: item.meta, - updated_at: item.updated_at - }, - create: item - }) - ) - ); - - return result.map(item => ({ - id: item.id, - tmdbId: item.tmdb_id, - userId: item.user_id, - seasonId: item.season_id, - episodeId: item.episode_id, - seasonNumber: item.season_number, - episodeNumber: item.episode_number, - meta: item.meta, - duration: Number(item.duration), - watched: Number(item.watched), - updatedAt: item.updated_at - })); }); \ No newline at end of file diff --git a/server/routes/users/[id]/sessions.ts b/server/routes/users/[id]/sessions.ts index a24423e..4d63613 100644 --- a/server/routes/users/[id]/sessions.ts +++ b/server/routes/users/[id]/sessions.ts @@ -18,12 +18,10 @@ export default defineEventHandler(async (event) => { return sessions.map(s => ({ id: s.id, - user: s.user, - createdAt: s.created_at, - accessedAt: s.accessed_at, - expiresAt: s.expires_at, + userId: s.user, + createdAt: s.created_at.toISOString(), + accessedAt: s.accessed_at.toISOString(), device: s.device, - userAgent: s.user_agent, - current: s.id === session.id + userAgent: s.user_agent })); }); \ No newline at end of file diff --git a/server/routes/users/[id]/settings.ts b/server/routes/users/[id]/settings.ts index 8be1ab3..3262dc1 100644 --- a/server/routes/users/[id]/settings.ts +++ b/server/routes/users/[id]/settings.ts @@ -1,13 +1,16 @@ import { useAuth } from '~/utils/auth'; import { z } from 'zod'; +import { scopedLogger } from '~/utils/logger'; + +const log = scopedLogger('user-settings'); const userSettingsSchema = z.object({ - application_theme: z.string().optional(), - application_language: z.string().optional(), - default_subtitle_language: z.string().optional(), - proxy_urls: z.array(z.string()).optional(), - trakt_key: z.string().optional(), - febbox_key: z.string().optional() + applicationTheme: z.string().nullable().optional(), + applicationLanguage: z.string(), + defaultSubtitleLanguage: z.string().nullable().optional(), + proxyUrls: z.array(z.string()).nullable().optional(), + traktKey: z.string().nullable().optional(), + febboxKey: z.string().nullable().optional() }); export default defineEventHandler(async (event) => { @@ -22,82 +25,93 @@ export default defineEventHandler(async (event) => { }); } - if (event.method === 'PUT') { - try { - const body = await readBody(event); - const validatedSettings = userSettingsSchema.parse(body); - - const existingSettings = await prisma.user_settings.findUnique({ - where: { id: userId } - }); - - let settings; - - if (existingSettings) { - settings = await prisma.user_settings.update({ - where: { id: userId }, - data: validatedSettings - }); - } else { - settings = await prisma.user_settings.create({ - data: { - id: userId, - ...validatedSettings - } - }); - } - - return { - settings: { - applicationTheme: settings.application_theme, - applicationLanguage: settings.application_language, - defaultSubtitleLanguage: settings.default_subtitle_language, - proxyUrls: settings.proxy_urls, - traktKey: settings.trakt_key, - febboxKey: settings.febbox_key - } - }; - } catch (error) { - if (error instanceof z.ZodError) { - throw createError({ - statusCode: 400, - message: 'Invalid settings data' - }); - } - - throw createError({ - statusCode: 500, - message: 'Failed to update settings' - }); - } - } else if (event.method === 'GET') { + if (event.method === 'GET') { const settings = await prisma.user_settings.findUnique({ where: { id: userId } }); - - if (!settings) { - return { - settings: { - applicationTheme: null, - applicationLanguage: null, - defaultSubtitleLanguage: null, - proxyUrls: [], - traktKey: null, - febboxKey: null - } - }; - } - + return { - settings: { + id: userId, + applicationTheme: settings?.application_theme || null, + applicationLanguage: settings?.application_language || 'en', + defaultSubtitleLanguage: settings?.default_subtitle_language || null, + proxyUrls: settings?.proxy_urls || null, + traktKey: settings?.trakt_key || null, + febboxKey: settings?.febbox_key || null + }; + } + + if (event.method === 'PUT') { + try { + const body = await readBody(event); + log.info('Updating user settings', { userId, body }); + + const validatedBody = userSettingsSchema.parse(body); + + // Handle proxyUrls properly - ensure it's an array or empty array when null + const proxyUrls = validatedBody.proxyUrls === null ? [] : (validatedBody.proxyUrls || []); + + const data = { + application_theme: validatedBody.applicationTheme ?? null, + application_language: validatedBody.applicationLanguage, + default_subtitle_language: validatedBody.defaultSubtitleLanguage ?? null, + proxy_urls: proxyUrls, + trakt_key: validatedBody.traktKey ?? null, + febbox_key: validatedBody.febboxKey ?? null + }; + + log.info('Preparing to upsert settings', { + userId, + data: { ...data, proxy_urls: Array.isArray(data.proxy_urls) ? data.proxy_urls.length : 'not an array' } + }); + + const settings = await prisma.user_settings.upsert({ + where: { id: userId }, + update: data, + create: { + id: userId, + ...data + } + }); + + log.info('Settings updated successfully', { userId }); + + return { + id: userId, applicationTheme: settings.application_theme, applicationLanguage: settings.application_language, defaultSubtitleLanguage: settings.default_subtitle_language, proxyUrls: settings.proxy_urls, traktKey: settings.trakt_key, febboxKey: settings.febbox_key + }; + } catch (error) { + if (error instanceof z.ZodError) { + log.error('Validation error in settings update', { + userId, + errors: error.errors + }); + + throw createError({ + statusCode: 400, + message: 'Invalid settings data', + cause: error.errors + }); } - }; + + // Log the specific error for debugging + log.error('Failed to update settings', { + userId, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined + }); + + throw createError({ + statusCode: 500, + message: 'Failed to update settings', + cause: error instanceof Error ? error.message : 'Unknown error' + }); + } } throw createError({