fix minor bugs and lint

This commit is contained in:
Pas 2025-05-05 09:53:17 -06:00
parent bd0ead6f6b
commit 04c823a126
50 changed files with 9645 additions and 656 deletions

120
.eslintrc.json Normal file
View file

@ -0,0 +1,120 @@
{
"root": true,
"env": {
"node": true,
"es2022": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint", "prettier"],
"rules": {
// Error prevention
"no-console": ["warn", { "allow": ["warn", "error"] }],
"no-debugger": "warn",
"no-duplicate-imports": "error",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
],
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/explicit-function-return-type": [
"warn",
{
"allowExpressions": true,
"allowTypedFunctionExpressions": true
}
],
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/prefer-nullish-coalescing": "error",
"@typescript-eslint/prefer-optional-chain": "error",
"@typescript-eslint/consistent-type-imports": [
"error",
{
"prefer": "type-imports",
"disallowTypeAnnotations": false
}
],
// Code style
"prettier/prettier": [
"error",
{
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2,
"semi": true
}
],
"arrow-body-style": ["error", "as-needed"],
"prefer-arrow-callback": "error",
"no-var": "error",
"prefer-const": "error",
"eqeqeq": ["error", "always", { "null": "ignore" }],
"no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 0 }],
"no-trailing-spaces": "error",
"eol-last": "error",
"comma-dangle": ["error", "always-multiline"],
"quotes": ["error", "single", { "avoidEscape": true }],
"semi": ["error", "always"],
"object-curly-spacing": ["error", "always"],
"array-bracket-spacing": ["error", "never"],
"computed-property-spacing": ["error", "never"],
"space-before-function-paren": [
"error",
{
"anonymous": "always",
"named": "never",
"asyncArrow": "always"
}
],
"space-before-blocks": ["error", "always"],
"keyword-spacing": ["error", { "before": true, "after": true }],
"space-infix-ops": "error",
"no-multi-spaces": "error",
"no-whitespace-before-property": "error",
"func-call-spacing": ["error", "never"],
"no-spaced-func": "error",
"no-unexpected-multiline": "error",
"no-mixed-spaces-and-tabs": "error",
"no-tabs": "error",
"indent": [
"error",
2,
{
"SwitchCase": 1,
"FunctionDeclaration": { "parameters": "first" },
"FunctionExpression": { "parameters": "first" },
"CallExpression": { "arguments": "first" },
"ArrayExpression": "first",
"ObjectExpression": "first"
}
]
},
"ignorePatterns": [
"node_modules/",
"dist/",
".nuxt/",
".output/",
"coverage/",
"*.config.js",
"*.config.ts"
]
}

View file

@ -1,7 +1,7 @@
name: Bug Report or Feature Request
description: File a bug report or request a new feature... llm template so sorry if it breaks
title: "[BUG/FEATURE]: "
labels: ["triage"]
title: '[BUG/FEATURE]: '
labels: ['triage']
body:
- type: markdown
attributes:

View file

@ -1,9 +1,11 @@
## Description
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context.
Fixes # (issue)
## Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)

10
.prettierrc Normal file
View file

@ -0,0 +1,10 @@
{
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2,
"semi": true,
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf"
}

View file

@ -2,11 +2,12 @@
[![Donate](https://img.shields.io/badge/Donate-GitHub%20Sponsors-PURPLE)](https://github.com/sponsors/FifthWit)
BackendV2 is a from scratch rewrite for the old Fastify and MikroOrm version with backwards compatibility!
## Tech Stack
This repo uses:
- [Nitro](https://nitro.build)
- [Prisma](https://pris.ly)
- [Zod](https://zod.dev)
@ -14,12 +15,15 @@ This repo uses:
along with other minor libraries, we chose Nitro for its fast DX, easy support for caching, minimal design, and rapid prototyping. Prisma due to it's clear syntax, typesafety, and popularity. Zod for validation.
# Goals
Since we've changed the codebase so much for better DX that comes with more changes!
- [ ] Recommendations using ML models to provide accurate Recommendations via embeddings using a vector database
- [x] Ratings, partly for the affirmentioned goal
- [ ] Client wrapper library for any site that wants to keep user data related to movies, films, and recommendations
## Minor information
Only make PRs to `beta` branch
Production deployments are [here](https://backend.fifthwit.net)
Beta deployments are [here](https://beta.backend.fifthwit.net)

View file

@ -3,7 +3,7 @@ services:
image: postgres:15-alpine
restart: unless-stopped
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U $$PG_USER -d $$PG_DB" ]
test: ['CMD-SHELL', 'pg_isready -U $$PG_USER -d $$PG_DB']
interval: 5s
retries: 10
timeout: 2s

View file

@ -1,21 +1,21 @@
import { config } from "dotenv";
import { config } from 'dotenv';
config();
import { version } from "./server/utils/config";
import { version } from './server/utils/config';
//https://nitro.unjs.io/config
export default defineNitroConfig({
srcDir: "server",
compatibilityDate: "2025-03-05",
srcDir: 'server',
compatibilityDate: '2025-03-05',
experimental: {
asyncContext: true,
},
runtimeConfig: {
public: {
meta: {
name: process.env.META_NAME || "",
description: process.env.META_DESCRIPTION || "",
version: version || "",
captcha: (process.env.CAPTCHA === "true").toString(),
captchaClientKey: process.env.CAPTCHA_CLIENT_KEY || "",
name: process.env.META_NAME || '',
description: process.env.META_DESCRIPTION || '',
version: version || '',
captcha: (process.env.CAPTCHA === 'true').toString(),
captchaClientKey: process.env.CAPTCHA_CLIENT_KEY || '',
},
},
cryptoSecret: process.env.CRYPTO_SECRET,

2066
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,13 @@
"preview": "node .output/server/index.mjs"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.31.1",
"@typescript-eslint/parser": "^8.31.1",
"eslint": "^9.26.0",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.4.0",
"nitropack": "latest",
"prettier": "^3.5.3",
"prisma": "^6.4.1"
},
"dependencies": {

6760
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,12 @@
export default defineEventHandler((event) => {
export default defineEventHandler(event => {
setResponseHeaders(event, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
'Access-Control-Allow-Headers': '*'
})
'Access-Control-Allow-Headers': '*',
});
if (event.method === 'OPTIONS') {
event.node.res.statusCode = 204
return ''
event.node.res.statusCode = 204;
return '';
}
})
});

View file

@ -4,15 +4,9 @@ import { scopedLogger } from '~/utils/logger';
const log = scopedLogger('metrics-middleware');
// Paths we don't want to track metrics for
const EXCLUDED_PATHS = [
'/metrics',
'/ping.txt',
'/favicon.ico',
'/robots.txt',
'/sitemap.xml'
];
const EXCLUDED_PATHS = ['/metrics', '/ping.txt', '/favicon.ico', '/robots.txt', '/sitemap.xml'];
export default defineEventHandler(async (event) => {
export default defineEventHandler(async event => {
// Skip tracking excluded paths
if (EXCLUDED_PATHS.includes(event.path)) {
return;
@ -41,7 +35,7 @@ export default defineEventHandler(async (event) => {
method,
route,
statusCode,
duration
duration,
});
}
});

View file

@ -11,14 +11,14 @@ const completeSchema = z.object({
device: z.string().max(500).min(1),
});
export default defineEventHandler(async (event) => {
export default defineEventHandler(async event => {
const body = await readBody(event);
const result = completeSchema.safeParse(body);
if (!result.success) {
throw createError({
statusCode: 400,
message: 'Invalid request body'
message: 'Invalid request body',
});
}
@ -32,19 +32,19 @@ export default defineEventHandler(async (event) => {
);
const user = await prisma.users.findUnique({
where: { public_key: body.publicKey }
where: { public_key: body.publicKey },
});
if (!user) {
throw createError({
statusCode: 401,
message: 'User cannot be found'
message: 'User cannot be found',
});
}
await prisma.users.update({
where: { id: user.id },
data: { last_logged_in: new Date() }
data: { last_logged_in: new Date() },
});
const auth = useAuth();
@ -58,7 +58,7 @@ export default defineEventHandler(async (event) => {
publicKey: user.public_key,
namespace: user.namespace,
profile: user.profile,
permissions: user.permissions
permissions: user.permissions,
},
session: {
id: session.id,
@ -67,8 +67,8 @@ export default defineEventHandler(async (event) => {
accessedAt: session.accessed_at,
expiresAt: session.expires_at,
device: session.device,
userAgent: session.user_agent
userAgent: session.user_agent,
},
token
token,
};
});

View file

@ -5,25 +5,25 @@ const startSchema = z.object({
publicKey: z.string(),
});
export default defineEventHandler(async (event) => {
export default defineEventHandler(async event => {
const body = await readBody(event);
const result = startSchema.safeParse(body);
if (!result.success) {
throw createError({
statusCode: 400,
message: 'Invalid request body'
message: 'Invalid request body',
});
}
const user = await prisma.users.findUnique({
where: { public_key: body.publicKey }
where: { public_key: body.publicKey },
});
if (!user) {
throw createError({
statusCode: 401,
message: 'User cannot be found'
message: 'User cannot be found',
});
}
@ -31,6 +31,6 @@ export default defineEventHandler(async (event) => {
const challengeCode = await challenge.createChallengeCode('login', 'mnemonic');
return {
challenge: challengeCode.code
challenge: challengeCode.code,
};
});

View file

@ -18,14 +18,14 @@ const completeSchema = z.object({
}),
});
export default defineEventHandler(async (event) => {
export default defineEventHandler(async event => {
const body = await readBody(event);
const result = completeSchema.safeParse(body);
if (!result.success) {
throw createError({
statusCode: 400,
message: 'Invalid request body'
message: 'Invalid request body',
});
}
@ -39,13 +39,13 @@ export default defineEventHandler(async (event) => {
);
const existingUser = await prisma.users.findUnique({
where: { public_key: body.publicKey }
where: { public_key: body.publicKey },
});
if (existingUser) {
throw createError({
statusCode: 409,
message: 'A user with this public key already exists'
message: 'A user with this public key already exists',
});
}
@ -60,8 +60,8 @@ export default defineEventHandler(async (event) => {
created_at: now,
last_logged_in: now,
permissions: [],
profile: body.profile
}
profile: body.profile,
},
});
const auth = useAuth();
@ -75,7 +75,7 @@ export default defineEventHandler(async (event) => {
publicKey: user.public_key,
namespace: user.namespace,
profile: user.profile,
permissions: user.permissions
permissions: user.permissions,
},
session: {
id: session.id,
@ -84,8 +84,8 @@ export default defineEventHandler(async (event) => {
accessedAt: session.accessed_at,
expiresAt: session.expires_at,
device: session.device,
userAgent: session.user_agent
userAgent: session.user_agent,
},
token
token,
};
});

View file

@ -5,14 +5,14 @@ const startSchema = z.object({
captchaToken: z.string().optional(),
});
export default defineEventHandler(async (event) => {
export default defineEventHandler(async event => {
const body = await readBody(event);
const result = startSchema.safeParse(body);
if (!result.success) {
throw createError({
statusCode: 400,
message: 'Invalid request body'
message: 'Invalid request body',
});
}
@ -20,6 +20,6 @@ export default defineEventHandler(async (event) => {
const challengeCode = await challenge.createChallengeCode('registration', 'mnemonic');
return {
challenge: challengeCode.code
challenge: challengeCode.code,
};
});

View file

@ -1,21 +1,19 @@
import { TMDB } from "tmdb-ts";
import { TMDB } from 'tmdb-ts';
const tmdb = new TMDB(useRuntimeConfig().tmdbApiKey);
import { trakt } from "#imports";
import { trakt } from '#imports';
export default defineCachedEventHandler(
async (event) => {
async event => {
const popular = { movies: [], shows: [] };
popular.movies.push(
...((data) => (
data.results.sort((a, b) => b.vote_average - a.vote_average),
data.results
))(await tmdb.movies.popular())
...(data => (data.results.sort((a, b) => b.vote_average - a.vote_average), data.results))(
await tmdb.movies.popular()
)
); // Sorts by vote average
popular.shows.push(
...((data) => (
data.results.sort((a, b) => b.vote_average - a.vote_average),
data.results
))(await tmdb.tvShows.popular())
...(data => (data.results.sort((a, b) => b.vote_average - a.vote_average), data.results))(
await tmdb.tvShows.popular()
)
); // Sorts by vote average
const genres = {
@ -44,7 +42,7 @@ export default defineCachedEventHandler(
for (let list = 0; list < internalLists.trending.length; list++) {
const items = await trakt.lists.items({
id: internalLists.trending[list].list.ids.trakt,
type: "all",
type: 'all',
});
lists.push({
name: internalLists.trending[list].list.name,
@ -55,7 +53,7 @@ export default defineCachedEventHandler(
switch (true) {
case !!items[item].movie?.ids?.tmdb:
lists[list].items.push({
type: "movie",
type: 'movie',
name: items[item].movie.title,
id: items[item].movie.ids.tmdb,
year: items[item].movie.year,
@ -63,7 +61,7 @@ export default defineCachedEventHandler(
break;
case !!items[item].show?.ids?.tmdb:
lists[list].items.push({
type: "show",
type: 'show',
name: items[item].show.title,
id: items[item].show.ids.tmdb,
year: items[item].show.year,
@ -76,7 +74,7 @@ export default defineCachedEventHandler(
for (let list = 0; list < internalLists.popular.length; list++) {
const items = await trakt.lists.items({
id: internalLists.popular[list].list.ids.trakt,
type: "all",
type: 'all',
});
lists.push({
name: internalLists.popular[list].list.name,
@ -87,7 +85,7 @@ export default defineCachedEventHandler(
switch (true) {
case !!items[item].movie?.ids?.tmdb:
lists[lists.length - 1].items.push({
type: "movie",
type: 'movie',
name: items[item].movie.title,
id: items[item].movie.ids.tmdb,
year: items[item].movie.year,
@ -95,7 +93,7 @@ export default defineCachedEventHandler(
break;
case !!items[item].show?.ids?.tmdb:
lists[lists.length - 1].items.push({
type: "show",
type: 'show',
name: items[item].show.title,
id: items[item].show.ids.tmdb,
year: items[item].show.year,
@ -123,6 +121,6 @@ export default defineCachedEventHandler(
};
},
{
maxAge: process.env.NODE_ENV === "production" ? 60 * 60 : 0, // 20 Minutes for prod, no cache for dev. Customize to your liking
maxAge: process.env.NODE_ENV === 'production' ? 60 * 60 : 0, // 20 Minutes for prod, no cache for dev. Customize to your liking
}
);

View file

@ -1,5 +1,5 @@
export default defineEventHandler((event) => {
export default defineEventHandler(event => {
return {
message: ``
message: ``,
};
});

View file

@ -1,6 +1,6 @@
import { version } from "~/utils/config";
export default defineEventHandler((event) => {
import { version } from '~/utils/config';
export default defineEventHandler(event => {
return {
message: `Backend is working as expected (v${version})`
message: `Backend is working as expected (v${version})`,
};
});

View file

@ -1,6 +1,6 @@
import { prisma } from "#imports";
import { prisma } from '#imports';
export default defineEventHandler(async (event) => {
export default defineEventHandler(async event => {
const id = event.context.params?.id;
const listInfo = await prisma.lists.findUnique({
where: {
@ -14,9 +14,9 @@ export default defineEventHandler(async (event) => {
if (!listInfo.public) {
return createError({
statusCode: 403,
message: "List is not public",
message: 'List is not public',
});
}
return listInfo;
})
});

View file

@ -1,10 +1,10 @@
const meta = useRuntimeConfig().public.meta
export default defineEventHandler((event) => {
const meta = useRuntimeConfig().public.meta;
export default defineEventHandler(event => {
return {
name: meta.name,
description: meta.description,
version: meta.version,
hasCaptcha: meta.captcha === 'true',
captchaClientKey: meta.captchaClientKey
}
})
captchaClientKey: meta.captchaClientKey,
};
});

View file

@ -16,14 +16,16 @@ async function ensureMetricsInitialized() {
}
}
export default defineEventHandler(async (event) => {
export default defineEventHandler(async event => {
try {
await ensureMetricsInitialized();
const body = await readBody(event);
const validatedBody = z.object({
const validatedBody = z
.object({
success: z.boolean(),
}).parse(body);
})
.parse(body);
recordCaptchaMetrics(validatedBody.success);
@ -31,11 +33,11 @@ export default defineEventHandler(async (event) => {
} catch (error) {
log.error('Failed to process captcha metrics', {
evt: 'metrics_error',
error: error instanceof Error ? error.message : String(error)
error: error instanceof Error ? error.message : String(error),
});
throw createError({
statusCode: error instanceof Error && error.message === 'metrics not initialized' ? 503 : 400,
message: error instanceof Error ? error.message : 'Failed to process metrics'
message: error instanceof Error ? error.message : 'Failed to process metrics',
});
}
});

View file

@ -15,7 +15,7 @@ async function ensureMetricsInitialized() {
}
}
export default defineEventHandler(async (event) => {
export default defineEventHandler(async event => {
try {
await ensureMetricsInitialized();
const metrics = await register.metrics();
@ -24,11 +24,11 @@ export default defineEventHandler(async (event) => {
} catch (error) {
log.error('Error in metrics endpoint:', {
evt: 'metrics_error',
error: error instanceof Error ? error.message : String(error)
error: error instanceof Error ? error.message : String(error),
});
throw createError({
statusCode: 500,
message: error instanceof Error ? error.message : 'Failed to collect metrics'
message: error instanceof Error ? error.message : 'Failed to collect metrics',
});
}
});

View file

@ -35,12 +35,12 @@ const metricsProviderInputSchema = z.object({
batchId: z.string().optional(),
});
export default defineEventHandler(async (event) => {
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'
message: 'Method not allowed',
});
}
@ -59,11 +59,11 @@ export default defineEventHandler(async (event) => {
} catch (error) {
log.error('Failed to process metrics', {
evt: 'metrics_error',
error: error instanceof Error ? error.message : String(error)
error: error instanceof Error ? error.message : String(error),
});
throw createError({
statusCode: error instanceof Error && error.message === 'metrics not initialized' ? 503 : 400,
message: error instanceof Error ? error.message : 'Failed to process metrics'
message: error instanceof Error ? error.message : 'Failed to process metrics',
});
}
});

View file

@ -5,13 +5,13 @@ const updateSessionSchema = z.object({
deviceName: z.string().max(500).min(1).optional(),
});
export default defineEventHandler(async (event) => {
export default defineEventHandler(async event => {
const sessionId = getRouterParam(event, 'sid');
const currentSession = await useAuth().getCurrentSession();
const targetedSession = await prisma.sessions.findUnique({
where: { id: sessionId }
where: { id: sessionId },
});
if (!targetedSession) {
@ -21,16 +21,17 @@ export default defineEventHandler(async (event) => {
throw createError({
statusCode: 404,
message: 'Session cannot be found'
message: 'Session cannot be found',
});
}
if (targetedSession.user !== currentSession.user) {
throw createError({
statusCode: 401,
message: event.method === 'DELETE'
message:
event.method === 'DELETE'
? 'Cannot delete sessions you do not own'
: 'Cannot edit sessions other than your own'
: 'Cannot edit sessions other than your own',
});
}
@ -42,13 +43,13 @@ export default defineEventHandler(async (event) => {
await prisma.sessions.update({
where: { id: sessionId },
data: {
device: validatedBody.deviceName
}
device: validatedBody.deviceName,
},
});
}
const updatedSession = await prisma.sessions.findUnique({
where: { id: sessionId }
where: { id: sessionId },
});
return {
@ -59,14 +60,14 @@ export default defineEventHandler(async (event) => {
expiresAt: updatedSession.expires_at,
device: updatedSession.device,
userAgent: updatedSession.user_agent,
current: updatedSession.id === currentSession.id
current: updatedSession.id === currentSession.id,
};
}
if (event.method === 'DELETE') {
const sid = event.context.params?.sid;
const sessionExists = await prisma.sessions.findUnique({
where: { id: sid }
where: { id: sid },
});
if (!sessionExists) {
@ -75,7 +76,7 @@ export default defineEventHandler(async (event) => {
const session = await useAuth().getSessionAndBump(sid);
await prisma.sessions.delete({
where: { id: sessionId }
where: { id: sessionId },
});
return { id: sessionId };
@ -83,6 +84,6 @@ export default defineEventHandler(async (event) => {
throw createError({
statusCode: 405,
message: 'Method not allowed'
message: 'Method not allowed',
});
});

View file

@ -1,16 +1,16 @@
import { useAuth } from '~/utils/auth';
export default defineEventHandler(async (event) => {
const session = await useAuth().getCurrentSession()
export default defineEventHandler(async event => {
const session = await useAuth().getCurrentSession();
const user = await prisma.users.findUnique({
where: { id: session.user }
where: { id: session.user },
});
if (!user) {
throw createError({
statusCode: 404,
message: 'User not found'
message: 'User not found',
});
}
@ -20,7 +20,7 @@ export default defineEventHandler(async (event) => {
publicKey: user.public_key,
namespace: user.namespace,
profile: user.profile,
permissions: user.permissions
permissions: user.permissions,
},
session: {
id: session.id,
@ -29,7 +29,7 @@ export default defineEventHandler(async (event) => {
accessedAt: session.accessed_at,
expiresAt: session.expires_at,
device: session.device,
userAgent: session.user_agent
}
userAgent: session.user_agent,
},
};
});

View file

@ -13,7 +13,7 @@ const bookmarkDataSchema = z.object({
meta: bookmarkMetaSchema,
});
export default defineEventHandler(async (event) => {
export default defineEventHandler(async event => {
const userId = event.context.params?.id;
const method = event.method;
@ -22,19 +22,19 @@ export default defineEventHandler(async (event) => {
if (session.user !== userId) {
throw createError({
statusCode: 403,
message: 'Cannot access other user information'
message: 'Cannot access other user information',
});
}
if (method === 'GET') {
const bookmarks = await prisma.bookmarks.findMany({
where: { user_id: userId }
where: { user_id: userId },
});
return bookmarks.map(bookmark => ({
tmdbId: bookmark.tmdb_id,
meta: bookmark.meta,
updatedAt: bookmark.updated_at
updatedAt: bookmark.updated_at,
}));
}
@ -50,25 +50,25 @@ export default defineEventHandler(async (event) => {
where: {
tmdb_id_user_id: {
tmdb_id: item.tmdbId,
user_id: userId
}
user_id: userId,
},
},
update: {
meta: item.meta,
updated_at: now
updated_at: now,
},
create: {
tmdb_id: item.tmdbId,
user_id: userId,
meta: item.meta,
updated_at: now
}
updated_at: now,
},
});
results.push({
tmdbId: bookmark.tmdb_id,
meta: bookmark.meta,
updatedAt: bookmark.updated_at
updatedAt: bookmark.updated_at,
});
}
@ -86,15 +86,15 @@ export default defineEventHandler(async (event) => {
where: {
tmdb_id_user_id: {
tmdb_id: tmdbId,
user_id: userId
}
}
user_id: userId,
},
},
});
if (existing) {
throw createError({
statusCode: 400,
message: 'Already bookmarked'
message: 'Already bookmarked',
});
}
@ -103,14 +103,14 @@ export default defineEventHandler(async (event) => {
tmdb_id: tmdbId,
user_id: userId,
meta: validatedBody.meta,
updated_at: new Date()
}
updated_at: new Date(),
},
});
return {
tmdbId: bookmark.tmdb_id,
meta: bookmark.meta,
updatedAt: bookmark.updated_at
updatedAt: bookmark.updated_at,
};
}
@ -120,19 +120,17 @@ export default defineEventHandler(async (event) => {
where: {
tmdb_id_user_id: {
tmdb_id: tmdbId,
user_id: userId
}
}
user_id: userId,
},
},
});
} catch (error) {
}
} catch (error) {}
return { tmdbId };
}
throw createError({
statusCode: 405,
message: 'Method not allowed'
message: 'Method not allowed',
});
});

View file

@ -8,16 +8,16 @@ const bookmarkMetaSchema = z.object({
title: z.string(),
year: z.number(),
poster: z.string().optional(),
type: z.enum(['movie', 'show'])
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()
tmdbId: z.string().optional(),
});
export default defineEventHandler(async (event) => {
export default defineEventHandler(async event => {
const userId = getRouterParam(event, 'id');
const tmdbId = getRouterParam(event, 'tmdbid');
@ -26,11 +26,11 @@ export default defineEventHandler(async (event) => {
if (session.user !== userId) {
throw createError({
statusCode: 403,
message: 'Cannot access bookmarks for other users'
message: 'Cannot access bookmarks for other users',
});
}
if (event.method === "POST") {
if (event.method === 'POST') {
try {
const body = await readBody(event);
log.info('Creating bookmark', { userId, tmdbId, body });
@ -49,8 +49,8 @@ export default defineEventHandler(async (event) => {
user_id: session.user,
tmdb_id: tmdbId,
meta: validatedMeta,
updated_at: new Date()
}
updated_at: new Date(),
},
});
log.info('Bookmark created successfully', { userId, tmdbId });
@ -58,25 +58,25 @@ export default defineEventHandler(async (event) => {
return {
tmdbId: bookmark.tmdb_id,
meta: bookmark.meta,
updatedAt: bookmark.updated_at
updatedAt: bookmark.updated_at,
};
} catch (error) {
log.error('Failed to create bookmark', {
userId,
tmdbId,
error: error instanceof Error ? error.message : String(error)
error: error instanceof Error ? error.message : String(error),
});
if (error instanceof z.ZodError) {
throw createError({
statusCode: 400,
message: JSON.stringify(error.errors, null, 2)
message: JSON.stringify(error.errors, null, 2),
});
}
throw error;
}
} else if (event.method === "DELETE") {
} else if (event.method === 'DELETE') {
log.info('Deleting bookmark', { userId, tmdbId });
try {
@ -84,9 +84,9 @@ export default defineEventHandler(async (event) => {
where: {
tmdb_id_user_id: {
tmdb_id: tmdbId,
user_id: session.user
}
}
user_id: session.user,
},
},
});
log.info('Bookmark deleted successfully', { userId, tmdbId });
@ -96,7 +96,7 @@ export default defineEventHandler(async (event) => {
log.error('Failed to delete bookmark', {
userId,
tmdbId,
error: error instanceof Error ? error.message : String(error)
error: error instanceof Error ? error.message : String(error),
});
// If bookmark doesn't exist, still return success
@ -106,6 +106,6 @@ export default defineEventHandler(async (event) => {
throw createError({
statusCode: 405,
message: 'Method not allowed'
message: 'Method not allowed',
});
});

View file

@ -8,11 +8,11 @@ const userProfileSchema = z.object({
profile: z.object({
icon: z.string(),
colorA: z.string(),
colorB: z.string()
})
colorB: z.string(),
}),
});
export default defineEventHandler(async (event) => {
export default defineEventHandler(async event => {
const userId = event.context.params?.id;
const session = await useAuth().getCurrentSession();
@ -20,7 +20,7 @@ export default defineEventHandler(async (event) => {
if (session.user !== userId) {
throw createError({
statusCode: 403,
message: 'Cannot modify other users'
message: 'Cannot modify other users',
});
}
@ -34,8 +34,8 @@ export default defineEventHandler(async (event) => {
const user = await prisma.users.update({
where: { id: userId },
data: {
profile: validatedBody.profile
}
profile: validatedBody.profile,
},
});
log.info('User profile updated successfully', { userId });
@ -47,26 +47,26 @@ export default defineEventHandler(async (event) => {
profile: user.profile,
permissions: user.permissions,
createdAt: user.created_at,
lastLoggedIn: user.last_logged_in
lastLoggedIn: user.last_logged_in,
};
} catch (error) {
log.error('Failed to update user profile', {
userId,
error: error instanceof Error ? error.message : String(error)
error: error instanceof Error ? error.message : String(error),
});
if (error instanceof z.ZodError) {
throw createError({
statusCode: 400,
message: 'Invalid profile data',
cause: error.errors
cause: error.errors,
});
}
throw createError({
statusCode: 500,
message: 'Failed to update user profile',
cause: error instanceof Error ? error.message : 'Unknown error'
cause: error instanceof Error ? error.message : 'Unknown error',
});
}
}
@ -76,26 +76,28 @@ export default defineEventHandler(async (event) => {
log.info('Deleting user account', { userId });
// Delete related records first
await prisma.$transaction(async (tx) => {
await prisma.$transaction(async tx => {
// Delete user bookmarks
await tx.bookmarks.deleteMany({
where: { user_id: userId }
where: { user_id: userId },
});
await tx.progress_items.deleteMany({
where: { user_id: userId }
where: { user_id: userId },
});
await tx.user_settings.delete({
where: { id: userId }
}).catch(() => {});
await tx.user_settings
.delete({
where: { id: userId },
})
.catch(() => {});
await tx.sessions.deleteMany({
where: { user: userId }
where: { user: userId },
});
await tx.users.delete({
where: { id: userId }
where: { id: userId },
});
});
@ -105,19 +107,19 @@ export default defineEventHandler(async (event) => {
} catch (error) {
log.error('Failed to delete user account', {
userId,
error: error instanceof Error ? error.message : String(error)
error: error instanceof Error ? error.message : String(error),
});
throw createError({
statusCode: 500,
message: 'Failed to delete user account',
cause: error instanceof Error ? error.message : 'Unknown error'
cause: error instanceof Error ? error.message : 'Unknown error',
});
}
}
throw createError({
statusCode: 405,
message: 'Method not allowed'
message: 'Method not allowed',
});
});

View file

@ -1,9 +1,9 @@
import { useAuth } from "#imports";
import { PrismaClient } from "@prisma/client";
import { useAuth } from '#imports';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default defineEventHandler(async (event) => {
export default defineEventHandler(async event => {
const userId = event.context.params?.id;
const listId = event.context.params?.listId;
const session = await useAuth().getCurrentSession();
@ -11,7 +11,7 @@ export default defineEventHandler(async (event) => {
if (session.user !== userId) {
throw createError({
statusCode: 403,
message: "Cannot delete lists for other users",
message: 'Cannot delete lists for other users',
});
}
const list = await prisma.lists.findUnique({
@ -21,7 +21,7 @@ export default defineEventHandler(async (event) => {
if (!list) {
throw createError({
statusCode: 404,
message: "List not found",
message: 'List not found',
});
}
@ -32,7 +32,7 @@ export default defineEventHandler(async (event) => {
});
}
await prisma.$transaction(async (tx) => {
await prisma.$transaction(async tx => {
await tx.list_items.deleteMany({
where: { list_id: listId },
});
@ -44,6 +44,6 @@ export default defineEventHandler(async (event) => {
return {
id: listId,
message: "List deleted successfully",
message: 'List deleted successfully',
};
});

View file

@ -1,16 +1,16 @@
import { useAuth } from "#imports";
import { PrismaClient } from "@prisma/client";
import { useAuth } from '#imports';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default defineEventHandler(async (event) => {
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 access other user information",
message: 'Cannot access other user information',
});
}

View file

@ -1,12 +1,12 @@
import { useAuth } from "#imports";
import { PrismaClient } from "@prisma/client";
import { z } from "zod";
import { useAuth } from '#imports';
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
const prisma = new PrismaClient();
const listItemSchema = z.object({
tmdb_id: z.string(),
type: z.enum(["movie", "tv"]),
type: z.enum(['movie', 'tv']),
});
const updateListSchema = z.object({
@ -18,14 +18,14 @@ const updateListSchema = z.object({
removeItems: z.array(listItemSchema).optional(),
});
export default defineEventHandler(async (event) => {
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 lists for other users",
message: 'Cannot modify lists for other users',
});
}
@ -40,7 +40,7 @@ export default defineEventHandler(async (event) => {
if (!list) {
throw createError({
statusCode: 404,
message: "List not found",
message: 'List not found',
});
}
@ -51,7 +51,7 @@ export default defineEventHandler(async (event) => {
});
}
const result = await prisma.$transaction(async (tx) => {
const result = await prisma.$transaction(async tx => {
if (
validatedBody.name ||
validatedBody.description !== undefined ||
@ -62,24 +62,22 @@ export default defineEventHandler(async (event) => {
data: {
name: validatedBody.name ?? list.name,
description:
validatedBody.description !== undefined
? validatedBody.description
: list.description,
validatedBody.description !== undefined ? validatedBody.description : list.description,
public: validatedBody.public ?? list.public,
},
});
}
if (validatedBody.addItems && validatedBody.addItems.length > 0) {
const existingTmdbIds = list.list_items.map((item) => item.tmdb_id);
const existingTmdbIds = list.list_items.map(item => item.tmdb_id);
const itemsToAdd = validatedBody.addItems.filter(
(item) => !existingTmdbIds.includes(item.tmdb_id)
item => !existingTmdbIds.includes(item.tmdb_id)
);
if (itemsToAdd.length > 0) {
await tx.list_items.createMany({
data: itemsToAdd.map((item) => ({
data: itemsToAdd.map(item => ({
list_id: list.id,
tmdb_id: item.tmdb_id,
type: item.type,
@ -90,9 +88,7 @@ export default defineEventHandler(async (event) => {
}
if (validatedBody.removeItems && validatedBody.removeItems.length > 0) {
const tmdbIdsToRemove = validatedBody.removeItems.map(
(item) => item.tmdb_id
);
const tmdbIdsToRemove = validatedBody.removeItems.map(item => item.tmdb_id);
await tx.list_items.deleteMany({
where: {
@ -110,6 +106,6 @@ export default defineEventHandler(async (event) => {
return {
list: result,
message: "List updated successfully",
message: 'List updated successfully',
};
});

View file

@ -1,12 +1,12 @@
import { useAuth } from "#imports";
import { PrismaClient } from "@prisma/client";
import { z } from "zod";
import { useAuth } from '#imports';
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
const prisma = new PrismaClient();
const listItemSchema = z.object({
tmdb_id: z.string(),
type: z.enum(["movie", "tv"]),
type: z.enum(['movie', 'tv']),
});
const createListSchema = z.object({
@ -16,14 +16,14 @@ const createListSchema = z.object({
public: z.boolean().optional(),
});
export default defineEventHandler(async (event) => {
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 user other than yourself",
message: 'Cannot modify user other than yourself',
});
}
@ -31,17 +31,17 @@ export default defineEventHandler(async (event) => {
let parsedBody;
try {
parsedBody = typeof body === "string" ? JSON.parse(body) : body;
parsedBody = typeof body === 'string' ? JSON.parse(body) : body;
} catch (error) {
throw createError({
statusCode: 400,
message: "Invalid request body format",
message: 'Invalid request body format',
});
}
const validatedBody = createListSchema.parse(parsedBody);
const result = await prisma.$transaction(async (tx) => {
const result = await prisma.$transaction(async tx => {
const newList = await tx.lists.create({
data: {
user_id: userId,
@ -53,7 +53,7 @@ export default defineEventHandler(async (event) => {
if (validatedBody.items && validatedBody.items.length > 0) {
await tx.list_items.createMany({
data: validatedBody.items.map((item) => ({
data: validatedBody.items.map(item => ({
list_id: newList.id,
tmdb_id: item.tmdb_id,
type: item.type, // Type is mapped here
@ -70,6 +70,6 @@ export default defineEventHandler(async (event) => {
return {
list: result,
message: "List created successfully",
message: 'List created successfully',
};
});

View file

@ -6,14 +6,14 @@ const progressMetaSchema = z.object({
title: z.string(),
year: z.number().optional(),
poster: z.string().optional(),
type: z.enum(['movie', 'show'])
type: z.enum(['movie', 'show']),
});
const progressItemSchema = z.object({
meta: progressMetaSchema,
tmdbId: z.string(),
duration: z.number().transform((n) => n.toString()),
watched: z.number().transform((n) => n.toString()),
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(),
@ -30,7 +30,7 @@ function defaultAndCoerceDateTime(dateTime: string | undefined) {
return new Date(clampedEpoch);
}
export default defineEventHandler(async (event) => {
export default defineEventHandler(async event => {
const userId = event.context.params?.id;
const method = event.method;
@ -38,20 +38,20 @@ export default defineEventHandler(async (event) => {
if (!session) {
throw createError({
statusCode: 401,
message: 'Session not found or expired'
message: 'Session not found or expired',
});
}
if (session.user !== userId) {
throw createError({
statusCode: 403,
message: 'Cannot access other user information'
message: 'Cannot access other user information',
});
}
if (method === 'GET') {
const items = await prisma.progress_items.findMany({
where: { user_id: userId }
where: { user_id: userId },
});
return items.map(item => ({
@ -59,16 +59,16 @@ export default defineEventHandler(async (event) => {
tmdbId: item.tmdb_id,
episode: {
id: item.episode_id || null,
number: item.episode_number || null
number: item.episode_number || null,
},
season: {
id: item.season_id || null,
number: item.season_number || null
number: item.season_number || null,
},
meta: item.meta,
duration: item.duration.toString(),
watched: item.watched.toString(),
updatedAt: item.updated_at.toISOString()
updatedAt: item.updated_at.toISOString(),
}));
}
@ -88,9 +88,9 @@ export default defineEventHandler(async (event) => {
tmdb_id: tmdbId,
user_id: userId,
season_id: validatedBody.seasonId || null,
episode_id: validatedBody.episodeId || null
}
}
episode_id: validatedBody.episodeId || null,
},
},
});
let progressItem;
@ -98,14 +98,14 @@ export default defineEventHandler(async (event) => {
if (existingItem) {
progressItem = await prisma.progress_items.update({
where: {
id: existingItem.id
id: existingItem.id,
},
data: {
duration: BigInt(validatedBody.duration),
watched: BigInt(validatedBody.watched),
meta: validatedBody.meta,
updated_at: now
}
updated_at: now,
},
});
} else {
progressItem = await prisma.progress_items.create({
@ -120,8 +120,8 @@ export default defineEventHandler(async (event) => {
duration: BigInt(validatedBody.duration),
watched: BigInt(validatedBody.watched),
meta: validatedBody.meta,
updated_at: now
}
updated_at: now,
},
});
}
@ -136,7 +136,7 @@ export default defineEventHandler(async (event) => {
meta: progressItem.meta,
duration: Number(progressItem.duration),
watched: Number(progressItem.watched),
updatedAt: progressItem.updated_at
updatedAt: progressItem.updated_at,
};
}
@ -145,14 +145,14 @@ export default defineEventHandler(async (event) => {
const whereClause: any = {
user_id: userId,
tmdb_id: tmdbId
tmdb_id: tmdbId,
};
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
where: whereClause,
});
if (itemsToDelete.length === 0) {
@ -160,25 +160,25 @@ export default defineEventHandler(async (event) => {
count: 0,
tmdbId,
episodeId: body.episodeId,
seasonId: body.seasonId
seasonId: body.seasonId,
};
}
await prisma.progress_items.deleteMany({
where: whereClause
where: whereClause,
});
return {
count: itemsToDelete.length,
tmdbId,
episodeId: body.episodeId,
seasonId: body.seasonId
seasonId: body.seasonId,
};
}
}
throw createError({
statusCode: 405,
message: 'Method not allowed'
message: 'Method not allowed',
});
});

View file

@ -6,14 +6,14 @@ const progressMetaSchema = z.object({
title: z.string(),
poster: z.string().optional(),
type: z.enum(['movie', 'tv', 'show']),
year: z.number().optional()
year: z.number().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().transform(n => Math.round(n)),
watched: z.number().transform(n => Math.round(n)),
seasonId: z.string().optional(),
episodeId: z.string().optional(),
seasonNumber: z.number().optional(),
@ -30,7 +30,7 @@ function defaultAndCoerceDateTime(dateTime: string | undefined) {
return new Date(clampedEpoch);
}
export default defineEventHandler(async (event) => {
export default defineEventHandler(async event => {
const userId = event.context.params?.id;
const tmdbId = event.context.params?.tmdb_id;
const method = event.method;
@ -40,7 +40,7 @@ export default defineEventHandler(async (event) => {
if (session.user !== userId) {
throw createError({
statusCode: 403,
message: 'Different userId than authenticated session'
message: 'Different userId than authenticated session',
});
}
@ -51,8 +51,8 @@ export default defineEventHandler(async (event) => {
const now = defaultAndCoerceDateTime(validatedBody.updatedAt);
const isMovie = validatedBody.meta.type === 'movie';
const seasonId = isMovie ? '\n' : (validatedBody.seasonId || null);
const episodeId = isMovie ? '\n' : (validatedBody.episodeId || null);
const seasonId = isMovie ? '\n' : validatedBody.seasonId || null;
const episodeId = isMovie ? '\n' : validatedBody.episodeId || null;
const existingItem = await prisma.progress_items.findUnique({
where: {
@ -60,9 +60,9 @@ export default defineEventHandler(async (event) => {
tmdb_id: tmdbId,
user_id: userId,
season_id: seasonId,
episode_id: episodeId
}
}
episode_id: episodeId,
},
},
});
let progressItem;
@ -70,14 +70,14 @@ export default defineEventHandler(async (event) => {
if (existingItem) {
progressItem = await prisma.progress_items.update({
where: {
id: existingItem.id
id: existingItem.id,
},
data: {
duration: BigInt(validatedBody.duration),
watched: BigInt(validatedBody.watched),
meta: validatedBody.meta,
updated_at: now
}
updated_at: now,
},
});
} else {
progressItem = await prisma.progress_items.create({
@ -92,8 +92,8 @@ export default defineEventHandler(async (event) => {
duration: BigInt(validatedBody.duration),
watched: BigInt(validatedBody.watched),
meta: validatedBody.meta,
updated_at: now
}
updated_at: now,
},
});
}
@ -108,14 +108,14 @@ export default defineEventHandler(async (event) => {
meta: progressItem.meta,
duration: Number(progressItem.duration),
watched: Number(progressItem.watched),
updatedAt: progressItem.updated_at
updatedAt: progressItem.updated_at,
};
} else if (method === 'DELETE') {
const body = await readBody(event).catch(() => ({}));
const whereClause: any = {
user_id: userId,
tmdb_id: tmdbId
tmdb_id: tmdbId,
};
if (body.seasonId) {
@ -131,7 +131,7 @@ export default defineEventHandler(async (event) => {
}
const itemsToDelete = await prisma.progress_items.findMany({
where: whereClause
where: whereClause,
});
if (itemsToDelete.length === 0) {
@ -139,24 +139,24 @@ export default defineEventHandler(async (event) => {
count: 0,
tmdbId,
episodeId: body.episodeId,
seasonId: body.seasonId
seasonId: body.seasonId,
};
}
await prisma.progress_items.deleteMany({
where: whereClause
where: whereClause,
});
return {
count: itemsToDelete.length,
tmdbId,
episodeId: body.episodeId,
seasonId: body.seasonId
seasonId: body.seasonId,
};
}
throw createError({
statusCode: 405,
message: 'Method not allowed'
message: 'Method not allowed',
});
});

View file

@ -9,19 +9,19 @@ const progressMetaSchema = z.object({
title: z.string(),
type: z.enum(['movie', 'show']),
year: z.number(),
poster: z.string().optional()
poster: z.string().optional(),
});
const progressItemSchema = z.object({
meta: progressMetaSchema,
tmdbId: z.string(),
duration: z.number(),
watched: z.number(),
duration: z.number().min(0),
watched: z.number().min(0),
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
@ -33,7 +33,7 @@ function defaultAndCoerceDateTime(dateTime: string | undefined) {
return new Date(clampedEpoch);
}
export default defineEventHandler(async (event) => {
export default defineEventHandler(async event => {
const userId = event.context.params?.id;
const session = await useAuth().getCurrentSession();
@ -41,14 +41,26 @@ export default defineEventHandler(async (event) => {
if (session.user !== userId) {
throw createError({
statusCode: 403,
message: 'Cannot modify user other than yourself'
message: 'Cannot modify user other than yourself',
});
}
// First check if user exists
const user = await prisma.users.findUnique({
where: { id: userId },
});
if (!user) {
throw createError({
statusCode: 404,
message: 'User not found',
});
}
if (event.method !== 'PUT') {
throw createError({
statusCode: 405,
message: 'Method not allowed'
message: 'Method not allowed',
});
}
@ -57,7 +69,7 @@ export default defineEventHandler(async (event) => {
const validatedBody = z.array(progressItemSchema).parse(body);
const existingItems = await prisma.progress_items.findMany({
where: { user_id: userId }
where: { user_id: userId },
});
const newItems = [...validatedBody];
@ -65,7 +77,7 @@ export default defineEventHandler(async (event) => {
for (const existingItem of existingItems) {
const newItemIndex = newItems.findIndex(
(item) =>
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)
@ -87,7 +99,7 @@ export default defineEventHandler(async (event) => {
duration: BigInt(newItem.duration),
watched: BigInt(newItem.watched),
meta: newItem.meta,
updated_at: defaultAndCoerceDateTime(newItem.updatedAt)
updated_at: defaultAndCoerceDateTime(newItem.updatedAt),
});
}
@ -102,14 +114,14 @@ export default defineEventHandler(async (event) => {
id: randomUUID(),
tmdb_id: item.tmdbId,
user_id: userId,
season_id: isMovie ? '\n' : (item.seasonId || null),
episode_id: isMovie ? '\n' : (item.episodeId || null),
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)
updated_at: defaultAndCoerceDateTime(item.updatedAt),
});
}
@ -123,16 +135,16 @@ export default defineEventHandler(async (event) => {
tmdb_id: item.tmdb_id,
user_id: item.user_id,
season_id: item.season_id,
episode_id: item.episode_id
}
episode_id: item.episode_id,
},
},
create: item,
update: {
duration: item.duration,
watched: item.watched,
meta: item.meta,
updated_at: item.updated_at
}
updated_at: item.updated_at,
},
});
results.push({
@ -140,22 +152,22 @@ export default defineEventHandler(async (event) => {
tmdbId: result.tmdb_id,
episode: {
id: result.episode_id === '\n' ? null : result.episode_id,
number: result.episode_number
number: result.episode_number,
},
season: {
id: result.season_id === '\n' ? null : result.season_id,
number: result.season_number
number: result.season_number,
},
meta: result.meta,
duration: result.duration.toString(),
watched: result.watched.toString(),
updatedAt: result.updated_at.toISOString()
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)
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
@ -165,17 +177,21 @@ export default defineEventHandler(async (event) => {
} catch (error) {
log.error('Failed to import progress', {
userId,
error: error instanceof Error ? error.message : String(error)
error: error instanceof Error ? error.message : String(error),
});
if (error instanceof z.ZodError) {
throw createError({
statusCode: 400,
message: 'Invalid progress data',
cause: error.errors
cause: error.errors,
});
}
throw error;
throw createError({
statusCode: 500,
message: 'Failed to import progress',
cause: error instanceof Error ? error.message : 'Unknown error',
});
}
});

View file

@ -4,10 +4,10 @@ import { z } from 'zod';
const userRatingsSchema = z.object({
tmdb_id: z.number(),
type: z.enum(['movie', 'tv']),
rating: z.number().min(0).max(10)
rating: z.number().min(0).max(10),
});
export default defineEventHandler(async (event) => {
export default defineEventHandler(async event => {
const userId = event.context.params?.id;
const session = await useAuth().getCurrentSession();
@ -15,34 +15,35 @@ export default defineEventHandler(async (event) => {
if (session.user !== userId) {
throw createError({
statusCode: 403,
message: 'Permission denied'
message: 'Permission denied',
});
}
if (event.method === 'GET'){
if (event.method === 'GET') {
const ratings = await prisma.users.findMany({
select: {
ratings: true
ratings: true,
},
where: {
id: userId
}});
id: userId,
},
});
return {
userId,
ratings: ratings[0].ratings
}
} else if (event.method === 'POST'){
ratings: ratings[0].ratings,
};
} else if (event.method === 'POST') {
const body = await readBody(event);
const validatedBody = userRatingsSchema.parse(body);
const user = await prisma.users.findUnique({
where: {
id: userId
id: userId,
},
select: {
ratings: true
}
ratings: true,
},
});
const userRatings = user?.ratings || [];
@ -62,11 +63,11 @@ export default defineEventHandler(async (event) => {
await prisma.users.update({
where: {
id: userId
id: userId,
},
data: {
ratings: updatedRatings
}
ratings: updatedRatings,
},
});
return {
@ -74,14 +75,14 @@ export default defineEventHandler(async (event) => {
rating: {
tmdb_id: validatedBody.tmdb_id,
type: validatedBody.type,
rating: validatedBody.rating
}
rating: validatedBody.rating,
},
};
}
// This should only execute if the method is neither GET nor POST
throw createError({
statusCode: 405,
message: 'Method not allowed'
message: 'Method not allowed',
});
})
});

View file

@ -1,5 +1,3 @@
export default defineEventHandler(() => {
return
})
return;
});

View file

@ -1,6 +1,6 @@
import { useAuth } from '~/utils/auth';
export default defineEventHandler(async (event) => {
export default defineEventHandler(async event => {
const userId = getRouterParam(event, 'id');
const session = await useAuth().getCurrentSession();
@ -8,12 +8,12 @@ export default defineEventHandler(async (event) => {
if (session.user !== userId) {
throw createError({
statusCode: 403,
message: 'Cannot access sessions for other users'
message: 'Cannot access sessions for other users',
});
}
const sessions = await prisma.sessions.findMany({
where: { user: userId }
where: { user: userId },
});
return sessions.map(s => ({
@ -22,6 +22,6 @@ export default defineEventHandler(async (event) => {
createdAt: s.created_at.toISOString(),
accessedAt: s.accessed_at.toISOString(),
device: s.device,
userAgent: s.user_agent
userAgent: s.user_agent,
}));
});

View file

@ -10,10 +10,10 @@ const userSettingsSchema = z.object({
defaultSubtitleLanguage: z.string().nullable().optional(),
proxyUrls: z.array(z.string()).nullable().optional(),
traktKey: z.string().nullable().optional(),
febboxKey: z.string().nullable().optional()
febboxKey: z.string().nullable().optional(),
});
export default defineEventHandler(async (event) => {
export default defineEventHandler(async event => {
const userId = event.context.params?.id;
const session = await useAuth().getCurrentSession();
@ -21,13 +21,26 @@ export default defineEventHandler(async (event) => {
if (session.user !== userId) {
throw createError({
statusCode: 403,
message: 'Permission denied'
message: 'Permission denied',
});
}
// First check if user exists
const user = await prisma.users.findUnique({
where: { id: userId },
});
if (!user) {
throw createError({
statusCode: 404,
message: 'User not found',
});
}
if (event.method === 'GET') {
try {
const settings = await prisma.user_settings.findUnique({
where: { id: userId }
where: { id: userId },
});
return {
@ -37,8 +50,18 @@ export default defineEventHandler(async (event) => {
defaultSubtitleLanguage: settings?.default_subtitle_language || null,
proxyUrls: settings?.proxy_urls.length === 0 ? null : settings?.proxy_urls || null,
traktKey: settings?.trakt_key || null,
febboxKey: settings?.febbox_key || null
febboxKey: settings?.febbox_key || null,
};
} catch (error) {
log.error('Failed to get user settings', {
userId,
error: error instanceof Error ? error.message : String(error),
});
throw createError({
statusCode: 500,
message: 'Failed to get user settings',
});
}
}
if (event.method === 'PUT') {
@ -54,7 +77,7 @@ export default defineEventHandler(async (event) => {
default_subtitle_language: validatedBody.defaultSubtitleLanguage ?? null,
proxy_urls: validatedBody.proxyUrls === null ? [] : validatedBody.proxyUrls || [],
trakt_key: validatedBody.traktKey ?? null,
febbox_key: validatedBody.febboxKey ?? null
febbox_key: validatedBody.febboxKey ?? null,
};
log.info('Preparing to upsert settings', { userId, data });
@ -64,8 +87,8 @@ export default defineEventHandler(async (event) => {
update: data,
create: {
id: userId,
...data
}
...data,
},
});
log.info('Settings updated successfully', { userId });
@ -77,39 +100,32 @@ export default defineEventHandler(async (event) => {
defaultSubtitleLanguage: settings.default_subtitle_language,
proxyUrls: settings.proxy_urls.length === 0 ? null : settings.proxy_urls,
traktKey: settings.trakt_key,
febboxKey: settings.febbox_key
febboxKey: settings.febbox_key,
};
} catch (error) {
if (error instanceof z.ZodError) {
log.error('Validation error in settings update', {
log.error('Failed to update user settings', {
userId,
errors: error.errors
error: error instanceof Error ? error.message : String(error),
});
if (error instanceof z.ZodError) {
throw createError({
statusCode: 400,
message: 'Invalid settings data',
cause: error.errors
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'
cause: error instanceof Error ? error.message : 'Unknown error',
});
}
}
throw createError({
statusCode: 405,
message: 'Method not allowed'
message: 'Method not allowed',
});
});

View file

@ -1 +1 @@
import { prisma } from '~/utils/prisma'
import { prisma } from '~/utils/prisma';

View file

@ -9,7 +9,7 @@ const SESSION_EXPIRY_MS = 21 * 24 * 60 * 60 * 1000;
export function useAuth() {
const getSession = async (id: string) => {
const session = await prisma.sessions.findUnique({
where: { id }
where: { id },
});
if (!session) return null;
@ -29,8 +29,8 @@ export function useAuth() {
where: { id },
data: {
accessed_at: now,
expires_at: expiryDate
}
expires_at: expiryDate,
},
});
};
@ -48,15 +48,15 @@ export function useAuth() {
user_agent: userAgent,
created_at: now,
accessed_at: now,
expires_at: expiryDate
}
expires_at: expiryDate,
},
});
};
const makeSessionToken = (session: { id: string }) => {
const runtimeConfig = useRuntimeConfig();
return sign({ sid: session.id }, runtimeConfig.cryptoSecret, {
algorithm: 'HS256'
algorithm: 'HS256',
});
};
@ -64,7 +64,7 @@ export function useAuth() {
try {
const runtimeConfig = useRuntimeConfig();
const payload = verify(token, runtimeConfig.cryptoSecret, {
algorithms: ['HS256']
algorithms: ['HS256'],
});
if (typeof payload === 'string') return null;
@ -80,7 +80,7 @@ export function useAuth() {
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw createError({
statusCode: 401,
message: 'Unauthorized'
message: 'Unauthorized',
});
}
@ -89,7 +89,7 @@ export function useAuth() {
if (!payload) {
throw createError({
statusCode: 401,
message: 'Invalid token'
message: 'Invalid token',
});
}
@ -97,7 +97,7 @@ export function useAuth() {
if (!session) {
throw createError({
statusCode: 401,
message: 'Session not found or expired'
message: 'Session not found or expired',
});
}
@ -110,6 +110,6 @@ export function useAuth() {
makeSession,
makeSessionToken,
verifySessionToken,
getCurrentSession
getCurrentSession,
};
}

View file

@ -16,8 +16,8 @@ export function useChallenge() {
flow,
auth_type: authType,
created_at: now,
expires_at: expiryDate
}
expires_at: expiryDate,
},
});
};
@ -29,7 +29,7 @@ export function useChallenge() {
authType: string
) => {
const challengeCode = await prisma.challenge_codes.findUnique({
where: { code }
where: { code },
});
if (!challengeCode) {
@ -50,7 +50,7 @@ export function useChallenge() {
}
await prisma.challenge_codes.delete({
where: { code }
where: { code },
});
return true;
@ -72,11 +72,7 @@ export function useChallenge() {
const publicKeyBuffer = Buffer.from(normalizedPublicKey, 'base64');
const messageBuffer = Buffer.from(data);
return nacl.sign.detached.verify(
messageBuffer,
signatureBuffer,
publicKeyBuffer
);
return nacl.sign.detached.verify(messageBuffer, signatureBuffer, publicKeyBuffer);
} catch (error) {
console.error('Signature verification error:', error);
return false;
@ -85,6 +81,6 @@ export function useChallenge() {
return {
createChallengeCode,
verifyChallengeCode
verifyChallengeCode,
};
}

View file

@ -1 +1 @@
export const version = '2.0.0'
export const version = '2.0.0';

View file

@ -89,21 +89,17 @@ async function saveMetricsToFile() {
if (!metrics) return;
const metricsData = await register.getMetricsAsJSON();
const relevantMetrics = metricsData.filter(metric =>
metric.name.startsWith('mw_') ||
metric.name === 'http_request_duration_seconds'
const relevantMetrics = metricsData.filter(
metric => metric.name.startsWith('mw_') || metric.name === 'http_request_duration_seconds'
);
fs.writeFileSync(
METRICS_FILE,
JSON.stringify(relevantMetrics, null, 2)
);
fs.writeFileSync(METRICS_FILE, JSON.stringify(relevantMetrics, null, 2));
log.info('Metrics saved to file', { evt: 'metrics_saved' });
} catch (error) {
log.error('Failed to save metrics', {
evt: 'save_metrics_error',
error: error instanceof Error ? error.message : String(error)
error: error instanceof Error ? error.message : String(error),
});
}
}
@ -119,13 +115,13 @@ async function loadMetricsFromFile(): Promise<any[]> {
const savedMetrics = JSON.parse(data);
log.info('Loaded saved metrics', {
evt: 'metrics_loaded',
count: savedMetrics.length
count: savedMetrics.length,
});
return savedMetrics;
} catch (error) {
log.error('Failed to load metrics', {
evt: 'load_metrics_error',
error: error instanceof Error ? error.message : String(error)
error: error instanceof Error ? error.message : String(error),
});
return [];
}
@ -172,9 +168,9 @@ export async function setupMetrics() {
const savedMetrics = await loadMetricsFromFile();
if (savedMetrics.length > 0) {
log.info('Restoring saved metrics...', { evt: 'restore_metrics' });
savedMetrics.forEach((metric) => {
savedMetrics.forEach(metric => {
if (metric.values) {
metric.values.forEach((value) => {
metric.values.forEach(value => {
switch (metric.name) {
case 'mw_user_count':
metrics?.user.inc(value.labels, value.value);
@ -196,8 +192,10 @@ export async function setupMetrics() {
break;
case 'http_request_duration_seconds':
// For histograms, special handling for sum and count
if (value.metricName === 'http_request_duration_seconds_sum' ||
value.metricName === 'http_request_duration_seconds_count') {
if (
value.metricName === 'http_request_duration_seconds_sum' ||
value.metricName === 'http_request_duration_seconds_count'
) {
metrics?.httpRequestDuration.observe(value.labels, value.value);
}
break;
@ -237,11 +235,11 @@ async function updateMetrics() {
metrics?.user.reset();
log.info('Reset user metrics counter', { evt: 'metrics_reset' });
users.forEach((v) => {
users.forEach(v => {
log.info('Incrementing user metric', {
evt: 'increment_metric',
namespace: v.namespace,
count: v._count
count: v._count,
});
metrics?.user.inc({ namespace: v.namespace }, v._count);
});
@ -250,20 +248,25 @@ async function updateMetrics() {
} catch (error) {
log.error('Failed to update metrics', {
evt: 'update_metrics_error',
error: error instanceof Error ? error.message : String(error)
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
// Export function to record HTTP request duration
export function recordHttpRequest(method: string, route: string, statusCode: number, duration: number) {
export function recordHttpRequest(
method: string,
route: string,
statusCode: number,
duration: number
) {
if (!metrics) return;
const labels = {
method,
route,
status_code: statusCode.toString()
status_code: statusCode.toString(),
};
// Record in both histogram and summary
@ -279,7 +282,7 @@ export function recordProviderMetrics(items: any[], hostname: string, tool?: str
metrics.providerHostnames.inc({ hostname });
// Record status and watch metrics for each item
items.forEach((item) => {
items.forEach(item => {
// Record provider status
metrics.providerStatuses.inc({
provider_id: item.embedId ?? item.providerId,

View file

@ -1,9 +1,9 @@
import { PrismaClient } from '@prisma/client'
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

View file

@ -1,8 +1,8 @@
import Trakt from "trakt.tv";
import Trakt from 'trakt.tv';
const traktKeys = useRuntimeConfig().trakt;
if (!traktKeys) {
throw new Error("Missing TraktKeys info ERROR: " + JSON.stringify(traktKeys));
throw new Error('Missing TraktKeys info ERROR: ' + JSON.stringify(traktKeys));
}
const options = {