mirror of
https://github.com/p-stream/backend.git
synced 2026-01-11 20:10:33 +00:00
fix minor bugs and lint
This commit is contained in:
parent
bd0ead6f6b
commit
04c823a126
50 changed files with 9645 additions and 656 deletions
120
.eslintrc.json
Normal file
120
.eslintrc.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
|
@ -1,3 +1,3 @@
|
||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: FifthWit
|
github: FifthWit
|
||||||
|
|
|
||||||
4
.github/ISSUE_TEMPLATE.yml
vendored
4
.github/ISSUE_TEMPLATE.yml
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
name: Bug Report or Feature Request
|
name: Bug Report or Feature Request
|
||||||
description: File a bug report or request a new feature... llm template so sorry if it breaks
|
description: File a bug report or request a new feature... llm template so sorry if it breaks
|
||||||
title: "[BUG/FEATURE]: "
|
title: '[BUG/FEATURE]: '
|
||||||
labels: ["triage"]
|
labels: ['triage']
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|
|
||||||
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -1,10 +1,12 @@
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context.
|
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context.
|
||||||
|
|
||||||
Fixes # (issue)
|
Fixes # (issue)
|
||||||
|
|
||||||
## Type of change
|
## Type of change
|
||||||
|
|
||||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||||
- [ ] New feature (non-breaking change which adds functionality)
|
- [ ] New feature (non-breaking change which adds functionality)
|
||||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
- [ ] Documentation update
|
- [ ] Documentation update
|
||||||
|
|
|
||||||
10
.prettierrc
Normal file
10
.prettierrc
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
|
|
@ -2,11 +2,12 @@
|
||||||
|
|
||||||
[](https://github.com/sponsors/FifthWit)
|
[](https://github.com/sponsors/FifthWit)
|
||||||
|
|
||||||
|
|
||||||
BackendV2 is a from scratch rewrite for the old Fastify and MikroOrm version with backwards compatibility!
|
BackendV2 is a from scratch rewrite for the old Fastify and MikroOrm version with backwards compatibility!
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
This repo uses:
|
This repo uses:
|
||||||
|
|
||||||
- [Nitro](https://nitro.build)
|
- [Nitro](https://nitro.build)
|
||||||
- [Prisma](https://pris.ly)
|
- [Prisma](https://pris.ly)
|
||||||
- [Zod](https://zod.dev)
|
- [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.
|
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
|
# Goals
|
||||||
|
|
||||||
Since we've changed the codebase so much for better DX that comes with more changes!
|
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
|
- [ ] Recommendations using ML models to provide accurate Recommendations via embeddings using a vector database
|
||||||
- [x] Ratings, partly for the affirmentioned goal
|
- [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
|
- [ ] Client wrapper library for any site that wants to keep user data related to movies, films, and recommendations
|
||||||
|
|
||||||
## Minor information
|
## Minor information
|
||||||
|
|
||||||
Only make PRs to `beta` branch
|
Only make PRs to `beta` branch
|
||||||
Production deployments are [here](https://backend.fifthwit.net)
|
Production deployments are [here](https://backend.fifthwit.net)
|
||||||
Beta deployments are [here](https://beta.backend.fifthwit.net)
|
Beta deployments are [here](https://beta.backend.fifthwit.net)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ services:
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
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
|
interval: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
timeout: 2s
|
timeout: 2s
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
import { config } from "dotenv";
|
import { config } from 'dotenv';
|
||||||
config();
|
config();
|
||||||
import { version } from "./server/utils/config";
|
import { version } from './server/utils/config';
|
||||||
//https://nitro.unjs.io/config
|
//https://nitro.unjs.io/config
|
||||||
export default defineNitroConfig({
|
export default defineNitroConfig({
|
||||||
srcDir: "server",
|
srcDir: 'server',
|
||||||
compatibilityDate: "2025-03-05",
|
compatibilityDate: '2025-03-05',
|
||||||
experimental: {
|
experimental: {
|
||||||
asyncContext: true,
|
asyncContext: true,
|
||||||
},
|
},
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
meta: {
|
meta: {
|
||||||
name: process.env.META_NAME || "",
|
name: process.env.META_NAME || '',
|
||||||
description: process.env.META_DESCRIPTION || "",
|
description: process.env.META_DESCRIPTION || '',
|
||||||
version: version || "",
|
version: version || '',
|
||||||
captcha: (process.env.CAPTCHA === "true").toString(),
|
captcha: (process.env.CAPTCHA === 'true').toString(),
|
||||||
captchaClientKey: process.env.CAPTCHA_CLIENT_KEY || "",
|
captchaClientKey: process.env.CAPTCHA_CLIENT_KEY || '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
cryptoSecret: process.env.CRYPTO_SECRET,
|
cryptoSecret: process.env.CRYPTO_SECRET,
|
||||||
|
|
|
||||||
2066
package-lock.json
generated
2066
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -7,7 +7,13 @@
|
||||||
"preview": "node .output/server/index.mjs"
|
"preview": "node .output/server/index.mjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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",
|
"nitropack": "latest",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
"prisma": "^6.4.1"
|
"prisma": "^6.4.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
6760
pnpm-lock.yaml
Normal file
6760
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +1,12 @@
|
||||||
export default defineEventHandler((event) => {
|
export default defineEventHandler(event => {
|
||||||
setResponseHeaders(event, {
|
setResponseHeaders(event, {
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||||
'Access-Control-Allow-Headers': '*'
|
'Access-Control-Allow-Headers': '*',
|
||||||
})
|
});
|
||||||
|
|
||||||
if (event.method === 'OPTIONS') {
|
if (event.method === 'OPTIONS') {
|
||||||
event.node.res.statusCode = 204
|
event.node.res.statusCode = 204;
|
||||||
return ''
|
return '';
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,22 +4,16 @@ import { scopedLogger } from '~/utils/logger';
|
||||||
const log = scopedLogger('metrics-middleware');
|
const log = scopedLogger('metrics-middleware');
|
||||||
|
|
||||||
// Paths we don't want to track metrics for
|
// Paths we don't want to track metrics for
|
||||||
const EXCLUDED_PATHS = [
|
const EXCLUDED_PATHS = ['/metrics', '/ping.txt', '/favicon.ico', '/robots.txt', '/sitemap.xml'];
|
||||||
'/metrics',
|
|
||||||
'/ping.txt',
|
|
||||||
'/favicon.ico',
|
|
||||||
'/robots.txt',
|
|
||||||
'/sitemap.xml'
|
|
||||||
];
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async event => {
|
||||||
// Skip tracking excluded paths
|
// Skip tracking excluded paths
|
||||||
if (EXCLUDED_PATHS.includes(event.path)) {
|
if (EXCLUDED_PATHS.includes(event.path)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = process.hrtime();
|
const start = process.hrtime();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Wait for the request to complete
|
// Wait for the request to complete
|
||||||
await event._handled;
|
await event._handled;
|
||||||
|
|
@ -27,12 +21,12 @@ export default defineEventHandler(async (event) => {
|
||||||
// Calculate duration once the response is sent
|
// Calculate duration once the response is sent
|
||||||
const [seconds, nanoseconds] = process.hrtime(start);
|
const [seconds, nanoseconds] = process.hrtime(start);
|
||||||
const duration = seconds + nanoseconds / 1e9;
|
const duration = seconds + nanoseconds / 1e9;
|
||||||
|
|
||||||
// Get cleaned route path (remove dynamic segments)
|
// Get cleaned route path (remove dynamic segments)
|
||||||
const method = event.method;
|
const method = event.method;
|
||||||
const route = getCleanPath(event.path);
|
const route = getCleanPath(event.path);
|
||||||
const statusCode = event.node.res.statusCode || 200;
|
const statusCode = event.node.res.statusCode || 200;
|
||||||
|
|
||||||
// Record the request metrics
|
// Record the request metrics
|
||||||
recordHttpRequest(method, route, statusCode, duration);
|
recordHttpRequest(method, route, statusCode, duration);
|
||||||
|
|
||||||
|
|
@ -41,7 +35,7 @@ export default defineEventHandler(async (event) => {
|
||||||
method,
|
method,
|
||||||
route,
|
route,
|
||||||
statusCode,
|
statusCode,
|
||||||
duration
|
duration,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -56,4 +50,4 @@ function getCleanPath(path: string): string {
|
||||||
.replace(/\/[^\/]+\/progress\/[^\/]+/, '/:uid/progress/:tmdbid')
|
.replace(/\/[^\/]+\/progress\/[^\/]+/, '/:uid/progress/:tmdbid')
|
||||||
.replace(/\/[^\/]+\/bookmarks\/[^\/]+/, '/:uid/bookmarks/:tmdbid')
|
.replace(/\/[^\/]+\/bookmarks\/[^\/]+/, '/:uid/bookmarks/:tmdbid')
|
||||||
.replace(/\/sessions\/[^\/]+/, '/sessions/:sid');
|
.replace(/\/sessions\/[^\/]+/, '/sessions/:sid');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,14 @@ const completeSchema = z.object({
|
||||||
device: z.string().max(500).min(1),
|
device: z.string().max(500).min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async event => {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
|
|
||||||
const result = completeSchema.safeParse(body);
|
const result = completeSchema.safeParse(body);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
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({
|
const user = await prisma.users.findUnique({
|
||||||
where: { public_key: body.publicKey }
|
where: { public_key: body.publicKey },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
message: 'User cannot be found'
|
message: 'User cannot be found',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.users.update({
|
await prisma.users.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
data: { last_logged_in: new Date() }
|
data: { last_logged_in: new Date() },
|
||||||
});
|
});
|
||||||
|
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
|
|
@ -58,7 +58,7 @@ export default defineEventHandler(async (event) => {
|
||||||
publicKey: user.public_key,
|
publicKey: user.public_key,
|
||||||
namespace: user.namespace,
|
namespace: user.namespace,
|
||||||
profile: user.profile,
|
profile: user.profile,
|
||||||
permissions: user.permissions
|
permissions: user.permissions,
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
|
|
@ -67,8 +67,8 @@ export default defineEventHandler(async (event) => {
|
||||||
accessedAt: session.accessed_at,
|
accessedAt: session.accessed_at,
|
||||||
expiresAt: session.expires_at,
|
expiresAt: session.expires_at,
|
||||||
device: session.device,
|
device: session.device,
|
||||||
userAgent: session.user_agent
|
userAgent: session.user_agent,
|
||||||
},
|
},
|
||||||
token
|
token,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,25 +5,25 @@ const startSchema = z.object({
|
||||||
publicKey: z.string(),
|
publicKey: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async event => {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
|
|
||||||
const result = startSchema.safeParse(body);
|
const result = startSchema.safeParse(body);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: 'Invalid request body'
|
message: 'Invalid request body',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.users.findUnique({
|
const user = await prisma.users.findUnique({
|
||||||
where: { public_key: body.publicKey }
|
where: { public_key: body.publicKey },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 401,
|
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');
|
const challengeCode = await challenge.createChallengeCode('login', 'mnemonic');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
challenge: challengeCode.code
|
challenge: challengeCode.code,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -18,17 +18,17 @@ const completeSchema = z.object({
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async event => {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
|
|
||||||
const result = completeSchema.safeParse(body);
|
const result = completeSchema.safeParse(body);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: 'Invalid request body'
|
message: 'Invalid request body',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const challenge = useChallenge();
|
const challenge = useChallenge();
|
||||||
await challenge.verifyChallengeCode(
|
await challenge.verifyChallengeCode(
|
||||||
body.challenge.code,
|
body.challenge.code,
|
||||||
|
|
@ -39,19 +39,19 @@ export default defineEventHandler(async (event) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const existingUser = await prisma.users.findUnique({
|
const existingUser = await prisma.users.findUnique({
|
||||||
where: { public_key: body.publicKey }
|
where: { public_key: body.publicKey },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 409,
|
statusCode: 409,
|
||||||
message: 'A user with this public key already exists'
|
message: 'A user with this public key already exists',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = randomUUID();
|
const userId = randomUUID();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
const user = await prisma.users.create({
|
const user = await prisma.users.create({
|
||||||
data: {
|
data: {
|
||||||
id: userId,
|
id: userId,
|
||||||
|
|
@ -60,22 +60,22 @@ export default defineEventHandler(async (event) => {
|
||||||
created_at: now,
|
created_at: now,
|
||||||
last_logged_in: now,
|
last_logged_in: now,
|
||||||
permissions: [],
|
permissions: [],
|
||||||
profile: body.profile
|
profile: body.profile,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
const userAgent = getRequestHeader(event, 'user-agent');
|
const userAgent = getRequestHeader(event, 'user-agent');
|
||||||
const session = await auth.makeSession(user.id, body.device, userAgent);
|
const session = await auth.makeSession(user.id, body.device, userAgent);
|
||||||
const token = auth.makeSessionToken(session);
|
const token = auth.makeSessionToken(session);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: {
|
user: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
publicKey: user.public_key,
|
publicKey: user.public_key,
|
||||||
namespace: user.namespace,
|
namespace: user.namespace,
|
||||||
profile: user.profile,
|
profile: user.profile,
|
||||||
permissions: user.permissions
|
permissions: user.permissions,
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
|
|
@ -84,8 +84,8 @@ export default defineEventHandler(async (event) => {
|
||||||
accessedAt: session.accessed_at,
|
accessedAt: session.accessed_at,
|
||||||
expiresAt: session.expires_at,
|
expiresAt: session.expires_at,
|
||||||
device: session.device,
|
device: session.device,
|
||||||
userAgent: session.user_agent
|
userAgent: session.user_agent,
|
||||||
},
|
},
|
||||||
token
|
token,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,14 @@ const startSchema = z.object({
|
||||||
captchaToken: z.string().optional(),
|
captchaToken: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async event => {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
|
|
||||||
const result = startSchema.safeParse(body);
|
const result = startSchema.safeParse(body);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
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');
|
const challengeCode = await challenge.createChallengeCode('registration', 'mnemonic');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
challenge: challengeCode.code
|
challenge: challengeCode.code,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,19 @@
|
||||||
import { TMDB } from "tmdb-ts";
|
import { TMDB } from 'tmdb-ts';
|
||||||
const tmdb = new TMDB(useRuntimeConfig().tmdbApiKey);
|
const tmdb = new TMDB(useRuntimeConfig().tmdbApiKey);
|
||||||
import { trakt } from "#imports";
|
import { trakt } from '#imports';
|
||||||
|
|
||||||
export default defineCachedEventHandler(
|
export default defineCachedEventHandler(
|
||||||
async (event) => {
|
async event => {
|
||||||
const popular = { movies: [], shows: [] };
|
const popular = { movies: [], shows: [] };
|
||||||
popular.movies.push(
|
popular.movies.push(
|
||||||
...((data) => (
|
...(data => (data.results.sort((a, b) => b.vote_average - a.vote_average), data.results))(
|
||||||
data.results.sort((a, b) => b.vote_average - a.vote_average),
|
await tmdb.movies.popular()
|
||||||
data.results
|
)
|
||||||
))(await tmdb.movies.popular())
|
|
||||||
); // Sorts by vote average
|
); // Sorts by vote average
|
||||||
popular.shows.push(
|
popular.shows.push(
|
||||||
...((data) => (
|
...(data => (data.results.sort((a, b) => b.vote_average - a.vote_average), data.results))(
|
||||||
data.results.sort((a, b) => b.vote_average - a.vote_average),
|
await tmdb.tvShows.popular()
|
||||||
data.results
|
)
|
||||||
))(await tmdb.tvShows.popular())
|
|
||||||
); // Sorts by vote average
|
); // Sorts by vote average
|
||||||
|
|
||||||
const genres = {
|
const genres = {
|
||||||
|
|
@ -44,7 +42,7 @@ export default defineCachedEventHandler(
|
||||||
for (let list = 0; list < internalLists.trending.length; list++) {
|
for (let list = 0; list < internalLists.trending.length; list++) {
|
||||||
const items = await trakt.lists.items({
|
const items = await trakt.lists.items({
|
||||||
id: internalLists.trending[list].list.ids.trakt,
|
id: internalLists.trending[list].list.ids.trakt,
|
||||||
type: "all",
|
type: 'all',
|
||||||
});
|
});
|
||||||
lists.push({
|
lists.push({
|
||||||
name: internalLists.trending[list].list.name,
|
name: internalLists.trending[list].list.name,
|
||||||
|
|
@ -55,7 +53,7 @@ export default defineCachedEventHandler(
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case !!items[item].movie?.ids?.tmdb:
|
case !!items[item].movie?.ids?.tmdb:
|
||||||
lists[list].items.push({
|
lists[list].items.push({
|
||||||
type: "movie",
|
type: 'movie',
|
||||||
name: items[item].movie.title,
|
name: items[item].movie.title,
|
||||||
id: items[item].movie.ids.tmdb,
|
id: items[item].movie.ids.tmdb,
|
||||||
year: items[item].movie.year,
|
year: items[item].movie.year,
|
||||||
|
|
@ -63,7 +61,7 @@ export default defineCachedEventHandler(
|
||||||
break;
|
break;
|
||||||
case !!items[item].show?.ids?.tmdb:
|
case !!items[item].show?.ids?.tmdb:
|
||||||
lists[list].items.push({
|
lists[list].items.push({
|
||||||
type: "show",
|
type: 'show',
|
||||||
name: items[item].show.title,
|
name: items[item].show.title,
|
||||||
id: items[item].show.ids.tmdb,
|
id: items[item].show.ids.tmdb,
|
||||||
year: items[item].show.year,
|
year: items[item].show.year,
|
||||||
|
|
@ -76,7 +74,7 @@ export default defineCachedEventHandler(
|
||||||
for (let list = 0; list < internalLists.popular.length; list++) {
|
for (let list = 0; list < internalLists.popular.length; list++) {
|
||||||
const items = await trakt.lists.items({
|
const items = await trakt.lists.items({
|
||||||
id: internalLists.popular[list].list.ids.trakt,
|
id: internalLists.popular[list].list.ids.trakt,
|
||||||
type: "all",
|
type: 'all',
|
||||||
});
|
});
|
||||||
lists.push({
|
lists.push({
|
||||||
name: internalLists.popular[list].list.name,
|
name: internalLists.popular[list].list.name,
|
||||||
|
|
@ -87,7 +85,7 @@ export default defineCachedEventHandler(
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case !!items[item].movie?.ids?.tmdb:
|
case !!items[item].movie?.ids?.tmdb:
|
||||||
lists[lists.length - 1].items.push({
|
lists[lists.length - 1].items.push({
|
||||||
type: "movie",
|
type: 'movie',
|
||||||
name: items[item].movie.title,
|
name: items[item].movie.title,
|
||||||
id: items[item].movie.ids.tmdb,
|
id: items[item].movie.ids.tmdb,
|
||||||
year: items[item].movie.year,
|
year: items[item].movie.year,
|
||||||
|
|
@ -95,7 +93,7 @@ export default defineCachedEventHandler(
|
||||||
break;
|
break;
|
||||||
case !!items[item].show?.ids?.tmdb:
|
case !!items[item].show?.ids?.tmdb:
|
||||||
lists[lists.length - 1].items.push({
|
lists[lists.length - 1].items.push({
|
||||||
type: "show",
|
type: 'show',
|
||||||
name: items[item].show.title,
|
name: items[item].show.title,
|
||||||
id: items[item].show.ids.tmdb,
|
id: items[item].show.ids.tmdb,
|
||||||
year: items[item].show.year,
|
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
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
export default defineEventHandler((event) => {
|
export default defineEventHandler(event => {
|
||||||
return {
|
return {
|
||||||
message: ``
|
message: ``,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { version } from "~/utils/config";
|
import { version } from '~/utils/config';
|
||||||
export default defineEventHandler((event) => {
|
export default defineEventHandler(event => {
|
||||||
return {
|
return {
|
||||||
message: `Backend is working as expected (v${version})`
|
message: `Backend is working as expected (v${version})`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
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 id = event.context.params?.id;
|
||||||
const listInfo = await prisma.lists.findUnique({
|
const listInfo = await prisma.lists.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
id: id,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
list_items: true,
|
list_items: true,
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!listInfo.public) {
|
||||||
|
return createError({
|
||||||
|
statusCode: 403,
|
||||||
|
message: 'List is not public',
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!listInfo.public) {
|
return listInfo;
|
||||||
return createError({
|
});
|
||||||
statusCode: 403,
|
|
||||||
message: "List is not public",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return listInfo;
|
|
||||||
})
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
const meta = useRuntimeConfig().public.meta
|
const meta = useRuntimeConfig().public.meta;
|
||||||
export default defineEventHandler((event) => {
|
export default defineEventHandler(event => {
|
||||||
return {
|
return {
|
||||||
name: meta.name,
|
name: meta.name,
|
||||||
description: meta.description,
|
description: meta.description,
|
||||||
version: meta.version,
|
version: meta.version,
|
||||||
hasCaptcha: meta.captcha === 'true',
|
hasCaptcha: meta.captcha === 'true',
|
||||||
captchaClientKey: meta.captchaClientKey
|
captchaClientKey: meta.captchaClientKey,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,16 @@ async function ensureMetricsInitialized() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async event => {
|
||||||
try {
|
try {
|
||||||
await ensureMetricsInitialized();
|
await ensureMetricsInitialized();
|
||||||
|
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
const validatedBody = z.object({
|
const validatedBody = z
|
||||||
success: z.boolean(),
|
.object({
|
||||||
}).parse(body);
|
success: z.boolean(),
|
||||||
|
})
|
||||||
|
.parse(body);
|
||||||
|
|
||||||
recordCaptchaMetrics(validatedBody.success);
|
recordCaptchaMetrics(validatedBody.success);
|
||||||
|
|
||||||
|
|
@ -31,11 +33,11 @@ export default defineEventHandler(async (event) => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Failed to process captcha metrics', {
|
log.error('Failed to process captcha metrics', {
|
||||||
evt: 'metrics_error',
|
evt: 'metrics_error',
|
||||||
error: error instanceof Error ? error.message : String(error)
|
error: error instanceof Error ? error.message : String(error),
|
||||||
});
|
});
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: error instanceof Error && error.message === 'metrics not initialized' ? 503 : 400,
|
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',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ async function ensureMetricsInitialized() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async event => {
|
||||||
try {
|
try {
|
||||||
await ensureMetricsInitialized();
|
await ensureMetricsInitialized();
|
||||||
const metrics = await register.metrics();
|
const metrics = await register.metrics();
|
||||||
|
|
@ -24,11 +24,11 @@ export default defineEventHandler(async (event) => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Error in metrics endpoint:', {
|
log.error('Error in metrics endpoint:', {
|
||||||
evt: 'metrics_error',
|
evt: 'metrics_error',
|
||||||
error: error instanceof Error ? error.message : String(error)
|
error: error instanceof Error ? error.message : String(error),
|
||||||
});
|
});
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
message: error instanceof Error ? error.message : 'Failed to collect metrics'
|
message: error instanceof Error ? error.message : 'Failed to collect metrics',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -35,23 +35,23 @@ const metricsProviderInputSchema = z.object({
|
||||||
batchId: z.string().optional(),
|
batchId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async event => {
|
||||||
// Handle both POST and PUT methods
|
// Handle both POST and PUT methods
|
||||||
if (event.method !== 'POST' && event.method !== 'PUT') {
|
if (event.method !== 'POST' && event.method !== 'PUT') {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 405,
|
statusCode: 405,
|
||||||
message: 'Method not allowed'
|
message: 'Method not allowed',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ensureMetricsInitialized();
|
await ensureMetricsInitialized();
|
||||||
|
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
const validatedBody = metricsProviderInputSchema.parse(body);
|
const validatedBody = metricsProviderInputSchema.parse(body);
|
||||||
|
|
||||||
const hostname = event.node.req.headers.origin?.slice(0, 255) ?? '<UNKNOWN>';
|
const hostname = event.node.req.headers.origin?.slice(0, 255) ?? '<UNKNOWN>';
|
||||||
|
|
||||||
// Use the simplified recordProviderMetrics function to handle all metrics recording
|
// Use the simplified recordProviderMetrics function to handle all metrics recording
|
||||||
recordProviderMetrics(validatedBody.items, hostname, validatedBody.tool);
|
recordProviderMetrics(validatedBody.items, hostname, validatedBody.tool);
|
||||||
|
|
||||||
|
|
@ -59,11 +59,11 @@ export default defineEventHandler(async (event) => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Failed to process metrics', {
|
log.error('Failed to process metrics', {
|
||||||
evt: 'metrics_error',
|
evt: 'metrics_error',
|
||||||
error: error instanceof Error ? error.message : String(error)
|
error: error instanceof Error ? error.message : String(error),
|
||||||
});
|
});
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: error instanceof Error && error.message === 'metrics not initialized' ? 503 : 400,
|
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',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
// Redirect to the POST handler which now supports both methods
|
// Redirect to the POST handler which now supports both methods
|
||||||
export { default } from './providers.post';
|
export { default } from './providers.post';
|
||||||
|
|
|
||||||
|
|
@ -5,52 +5,53 @@ const updateSessionSchema = z.object({
|
||||||
deviceName: z.string().max(500).min(1).optional(),
|
deviceName: z.string().max(500).min(1).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async event => {
|
||||||
const sessionId = getRouterParam(event, 'sid');
|
const sessionId = getRouterParam(event, 'sid');
|
||||||
|
|
||||||
const currentSession = await useAuth().getCurrentSession();
|
const currentSession = await useAuth().getCurrentSession();
|
||||||
|
|
||||||
const targetedSession = await prisma.sessions.findUnique({
|
const targetedSession = await prisma.sessions.findUnique({
|
||||||
where: { id: sessionId }
|
where: { id: sessionId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!targetedSession) {
|
if (!targetedSession) {
|
||||||
if (event.method === 'DELETE') {
|
if (event.method === 'DELETE') {
|
||||||
return { id: sessionId };
|
return { id: sessionId };
|
||||||
}
|
}
|
||||||
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 404,
|
statusCode: 404,
|
||||||
message: 'Session cannot be found'
|
message: 'Session cannot be found',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetedSession.user !== currentSession.user) {
|
if (targetedSession.user !== currentSession.user) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
message: event.method === 'DELETE'
|
message:
|
||||||
? 'Cannot delete sessions you do not own'
|
event.method === 'DELETE'
|
||||||
: 'Cannot edit sessions other than your own'
|
? 'Cannot delete sessions you do not own'
|
||||||
|
: 'Cannot edit sessions other than your own',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.method === 'PATCH') {
|
if (event.method === 'PATCH') {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
const validatedBody = updateSessionSchema.parse(body);
|
const validatedBody = updateSessionSchema.parse(body);
|
||||||
|
|
||||||
if (validatedBody.deviceName) {
|
if (validatedBody.deviceName) {
|
||||||
await prisma.sessions.update({
|
await prisma.sessions.update({
|
||||||
where: { id: sessionId },
|
where: { id: sessionId },
|
||||||
data: {
|
data: {
|
||||||
device: validatedBody.deviceName
|
device: validatedBody.deviceName,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedSession = await prisma.sessions.findUnique({
|
const updatedSession = await prisma.sessions.findUnique({
|
||||||
where: { id: sessionId }
|
where: { id: sessionId },
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: updatedSession.id,
|
id: updatedSession.id,
|
||||||
user: updatedSession.user,
|
user: updatedSession.user,
|
||||||
|
|
@ -59,30 +60,30 @@ export default defineEventHandler(async (event) => {
|
||||||
expiresAt: updatedSession.expires_at,
|
expiresAt: updatedSession.expires_at,
|
||||||
device: updatedSession.device,
|
device: updatedSession.device,
|
||||||
userAgent: updatedSession.user_agent,
|
userAgent: updatedSession.user_agent,
|
||||||
current: updatedSession.id === currentSession.id
|
current: updatedSession.id === currentSession.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.method === 'DELETE') {
|
if (event.method === 'DELETE') {
|
||||||
const sid = event.context.params?.sid;
|
const sid = event.context.params?.sid;
|
||||||
const sessionExists = await prisma.sessions.findUnique({
|
const sessionExists = await prisma.sessions.findUnique({
|
||||||
where: { id: sid }
|
where: { id: sid },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!sessionExists) {
|
if (!sessionExists) {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
const session = await useAuth().getSessionAndBump(sid);
|
const session = await useAuth().getSessionAndBump(sid);
|
||||||
|
|
||||||
await prisma.sessions.delete({
|
await prisma.sessions.delete({
|
||||||
where: { id: sessionId }
|
where: { id: sessionId },
|
||||||
});
|
});
|
||||||
|
|
||||||
return { id: sessionId };
|
return { id: sessionId };
|
||||||
}
|
}
|
||||||
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 405,
|
statusCode: 405,
|
||||||
message: 'Method not allowed'
|
message: 'Method not allowed',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { useAuth } from '~/utils/auth';
|
import { useAuth } from '~/utils/auth';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async event => {
|
||||||
const session = await useAuth().getCurrentSession()
|
const session = await useAuth().getCurrentSession();
|
||||||
|
|
||||||
const user = await prisma.users.findUnique({
|
const user = await prisma.users.findUnique({
|
||||||
where: { id: session.user }
|
where: { id: session.user },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 404,
|
statusCode: 404,
|
||||||
message: 'User not found'
|
message: 'User not found',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -20,7 +20,7 @@ export default defineEventHandler(async (event) => {
|
||||||
publicKey: user.public_key,
|
publicKey: user.public_key,
|
||||||
namespace: user.namespace,
|
namespace: user.namespace,
|
||||||
profile: user.profile,
|
profile: user.profile,
|
||||||
permissions: user.permissions
|
permissions: user.permissions,
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
|
|
@ -29,7 +29,7 @@ export default defineEventHandler(async (event) => {
|
||||||
accessedAt: session.accessed_at,
|
accessedAt: session.accessed_at,
|
||||||
expiresAt: session.expires_at,
|
expiresAt: session.expires_at,
|
||||||
device: session.device,
|
device: session.device,
|
||||||
userAgent: session.user_agent
|
userAgent: session.user_agent,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -13,126 +13,124 @@ const bookmarkDataSchema = z.object({
|
||||||
meta: bookmarkMetaSchema,
|
meta: bookmarkMetaSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async event => {
|
||||||
const userId = event.context.params?.id;
|
const userId = event.context.params?.id;
|
||||||
const method = event.method;
|
const method = event.method;
|
||||||
|
|
||||||
const session = await useAuth().getCurrentSession();
|
const session = await useAuth().getCurrentSession();
|
||||||
|
|
||||||
if (session.user !== userId) {
|
if (session.user !== userId) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 403,
|
statusCode: 403,
|
||||||
message: 'Cannot access other user information'
|
message: 'Cannot access other user information',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method === 'GET') {
|
if (method === 'GET') {
|
||||||
const bookmarks = await prisma.bookmarks.findMany({
|
const bookmarks = await prisma.bookmarks.findMany({
|
||||||
where: { user_id: userId }
|
where: { user_id: userId },
|
||||||
});
|
});
|
||||||
|
|
||||||
return bookmarks.map(bookmark => ({
|
return bookmarks.map(bookmark => ({
|
||||||
tmdbId: bookmark.tmdb_id,
|
tmdbId: bookmark.tmdb_id,
|
||||||
meta: bookmark.meta,
|
meta: bookmark.meta,
|
||||||
updatedAt: bookmark.updated_at
|
updatedAt: bookmark.updated_at,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method === 'PUT') {
|
if (method === 'PUT') {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
const validatedBody = z.array(bookmarkDataSchema).parse(body);
|
const validatedBody = z.array(bookmarkDataSchema).parse(body);
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
for (const item of validatedBody) {
|
for (const item of validatedBody) {
|
||||||
const bookmark = await prisma.bookmarks.upsert({
|
const bookmark = await prisma.bookmarks.upsert({
|
||||||
where: {
|
where: {
|
||||||
tmdb_id_user_id: {
|
tmdb_id_user_id: {
|
||||||
tmdb_id: item.tmdbId,
|
tmdb_id: item.tmdbId,
|
||||||
user_id: userId
|
user_id: userId,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
meta: item.meta,
|
meta: item.meta,
|
||||||
updated_at: now
|
updated_at: now,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
tmdb_id: item.tmdbId,
|
tmdb_id: item.tmdbId,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
meta: item.meta,
|
meta: item.meta,
|
||||||
updated_at: now
|
updated_at: now,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
tmdbId: bookmark.tmdb_id,
|
tmdbId: bookmark.tmdb_id,
|
||||||
meta: bookmark.meta,
|
meta: bookmark.meta,
|
||||||
updatedAt: bookmark.updated_at
|
updatedAt: bookmark.updated_at,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
const segments = event.path.split('/');
|
const segments = event.path.split('/');
|
||||||
const tmdbId = segments[segments.length - 1];
|
const tmdbId = segments[segments.length - 1];
|
||||||
|
|
||||||
if (method === 'POST') {
|
if (method === 'POST') {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
const validatedBody = bookmarkDataSchema.parse(body);
|
const validatedBody = bookmarkDataSchema.parse(body);
|
||||||
|
|
||||||
const existing = await prisma.bookmarks.findUnique({
|
const existing = await prisma.bookmarks.findUnique({
|
||||||
where: {
|
where: {
|
||||||
tmdb_id_user_id: {
|
tmdb_id_user_id: {
|
||||||
tmdb_id: tmdbId,
|
tmdb_id: tmdbId,
|
||||||
user_id: userId
|
user_id: userId,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: 'Already bookmarked'
|
message: 'Already bookmarked',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const bookmark = await prisma.bookmarks.create({
|
const bookmark = await prisma.bookmarks.create({
|
||||||
data: {
|
data: {
|
||||||
tmdb_id: tmdbId,
|
tmdb_id: tmdbId,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
meta: validatedBody.meta,
|
meta: validatedBody.meta,
|
||||||
updated_at: new Date()
|
updated_at: new Date(),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tmdbId: bookmark.tmdb_id,
|
tmdbId: bookmark.tmdb_id,
|
||||||
meta: bookmark.meta,
|
meta: bookmark.meta,
|
||||||
updatedAt: bookmark.updated_at
|
updatedAt: bookmark.updated_at,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method === 'DELETE') {
|
if (method === 'DELETE') {
|
||||||
try {
|
try {
|
||||||
await prisma.bookmarks.delete({
|
await prisma.bookmarks.delete({
|
||||||
where: {
|
where: {
|
||||||
tmdb_id_user_id: {
|
tmdb_id_user_id: {
|
||||||
tmdb_id: tmdbId,
|
tmdb_id: tmdbId,
|
||||||
user_id: userId
|
user_id: userId,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return { tmdbId };
|
return { tmdbId };
|
||||||
}
|
}
|
||||||
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 405,
|
statusCode: 405,
|
||||||
message: 'Method not allowed'
|
message: 'Method not allowed',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,16 @@ const bookmarkMetaSchema = z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
year: z.number(),
|
year: z.number(),
|
||||||
poster: z.string().optional(),
|
poster: z.string().optional(),
|
||||||
type: z.enum(['movie', 'show'])
|
type: z.enum(['movie', 'show']),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Support both formats: direct fields or nested under meta
|
// Support both formats: direct fields or nested under meta
|
||||||
const bookmarkRequestSchema = z.object({
|
const bookmarkRequestSchema = z.object({
|
||||||
meta: bookmarkMetaSchema.optional(),
|
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 userId = getRouterParam(event, 'id');
|
||||||
const tmdbId = getRouterParam(event, 'tmdbid');
|
const tmdbId = getRouterParam(event, 'tmdbid');
|
||||||
|
|
||||||
|
|
@ -26,86 +26,86 @@ export default defineEventHandler(async (event) => {
|
||||||
if (session.user !== userId) {
|
if (session.user !== userId) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 403,
|
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 {
|
try {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
log.info('Creating bookmark', { userId, tmdbId, body });
|
log.info('Creating bookmark', { userId, tmdbId, body });
|
||||||
|
|
||||||
// Parse and validate the request body
|
// Parse and validate the request body
|
||||||
const validatedRequest = bookmarkRequestSchema.parse(body);
|
const validatedRequest = bookmarkRequestSchema.parse(body);
|
||||||
|
|
||||||
// Extract the meta data - either directly from meta field or from the root
|
// Extract the meta data - either directly from meta field or from the root
|
||||||
const metaData = validatedRequest.meta || body;
|
const metaData = validatedRequest.meta || body;
|
||||||
|
|
||||||
// Validate the meta data separately
|
// Validate the meta data separately
|
||||||
const validatedMeta = bookmarkMetaSchema.parse(metaData);
|
const validatedMeta = bookmarkMetaSchema.parse(metaData);
|
||||||
|
|
||||||
const bookmark = await prisma.bookmarks.create({
|
const bookmark = await prisma.bookmarks.create({
|
||||||
data: {
|
data: {
|
||||||
user_id: session.user,
|
user_id: session.user,
|
||||||
tmdb_id: tmdbId,
|
tmdb_id: tmdbId,
|
||||||
meta: validatedMeta,
|
meta: validatedMeta,
|
||||||
updated_at: new Date()
|
updated_at: new Date(),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info('Bookmark created successfully', { userId, tmdbId });
|
log.info('Bookmark created successfully', { userId, tmdbId });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tmdbId: bookmark.tmdb_id,
|
tmdbId: bookmark.tmdb_id,
|
||||||
meta: bookmark.meta,
|
meta: bookmark.meta,
|
||||||
updatedAt: bookmark.updated_at
|
updatedAt: bookmark.updated_at,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Failed to create bookmark', {
|
log.error('Failed to create bookmark', {
|
||||||
userId,
|
userId,
|
||||||
tmdbId,
|
tmdbId,
|
||||||
error: error instanceof Error ? error.message : String(error)
|
error: error instanceof Error ? error.message : String(error),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: JSON.stringify(error.errors, null, 2)
|
message: JSON.stringify(error.errors, null, 2),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
} else if (event.method === "DELETE") {
|
} else if (event.method === 'DELETE') {
|
||||||
log.info('Deleting bookmark', { userId, tmdbId });
|
log.info('Deleting bookmark', { userId, tmdbId });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await prisma.bookmarks.delete({
|
await prisma.bookmarks.delete({
|
||||||
where: {
|
where: {
|
||||||
tmdb_id_user_id: {
|
tmdb_id_user_id: {
|
||||||
tmdb_id: tmdbId,
|
tmdb_id: tmdbId,
|
||||||
user_id: session.user
|
user_id: session.user,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info('Bookmark deleted successfully', { userId, tmdbId });
|
log.info('Bookmark deleted successfully', { userId, tmdbId });
|
||||||
|
|
||||||
return { success: true, tmdbId };
|
return { success: true, tmdbId };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Failed to delete bookmark', {
|
log.error('Failed to delete bookmark', {
|
||||||
userId,
|
userId,
|
||||||
tmdbId,
|
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
|
// If bookmark doesn't exist, still return success
|
||||||
return { success: true, tmdbId };
|
return { success: true, tmdbId };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 405,
|
statusCode: 405,
|
||||||
message: 'Method not allowed'
|
message: 'Method not allowed',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,19 +8,19 @@ const userProfileSchema = z.object({
|
||||||
profile: z.object({
|
profile: z.object({
|
||||||
icon: z.string(),
|
icon: z.string(),
|
||||||
colorA: 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 userId = event.context.params?.id;
|
||||||
|
|
||||||
const session = await useAuth().getCurrentSession();
|
const session = await useAuth().getCurrentSession();
|
||||||
|
|
||||||
if (session.user !== userId) {
|
if (session.user !== userId) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 403,
|
statusCode: 403,
|
||||||
message: 'Cannot modify other users'
|
message: 'Cannot modify other users',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,18 +28,18 @@ export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
log.info('Updating user profile', { userId, body });
|
log.info('Updating user profile', { userId, body });
|
||||||
|
|
||||||
const validatedBody = userProfileSchema.parse(body);
|
const validatedBody = userProfileSchema.parse(body);
|
||||||
|
|
||||||
const user = await prisma.users.update({
|
const user = await prisma.users.update({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
data: {
|
data: {
|
||||||
profile: validatedBody.profile
|
profile: validatedBody.profile,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info('User profile updated successfully', { userId });
|
log.info('User profile updated successfully', { userId });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
publicKey: user.public_key,
|
publicKey: user.public_key,
|
||||||
|
|
@ -47,77 +47,79 @@ export default defineEventHandler(async (event) => {
|
||||||
profile: user.profile,
|
profile: user.profile,
|
||||||
permissions: user.permissions,
|
permissions: user.permissions,
|
||||||
createdAt: user.created_at,
|
createdAt: user.created_at,
|
||||||
lastLoggedIn: user.last_logged_in
|
lastLoggedIn: user.last_logged_in,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Failed to update user profile', {
|
log.error('Failed to update user profile', {
|
||||||
userId,
|
userId,
|
||||||
error: error instanceof Error ? error.message : String(error)
|
error: error instanceof Error ? error.message : String(error),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: 'Invalid profile data',
|
message: 'Invalid profile data',
|
||||||
cause: error.errors
|
cause: error.errors,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
message: 'Failed to update user profile',
|
message: 'Failed to update user profile',
|
||||||
cause: error instanceof Error ? error.message : 'Unknown error'
|
cause: error instanceof Error ? error.message : 'Unknown error',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.method === 'DELETE') {
|
if (event.method === 'DELETE') {
|
||||||
try {
|
try {
|
||||||
log.info('Deleting user account', { userId });
|
log.info('Deleting user account', { userId });
|
||||||
|
|
||||||
// Delete related records first
|
// Delete related records first
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async tx => {
|
||||||
// Delete user bookmarks
|
// Delete user bookmarks
|
||||||
await tx.bookmarks.deleteMany({
|
await tx.bookmarks.deleteMany({
|
||||||
where: { user_id: userId }
|
where: { user_id: userId },
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.progress_items.deleteMany({
|
await tx.progress_items.deleteMany({
|
||||||
where: { user_id: userId }
|
where: { user_id: userId },
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.user_settings.delete({
|
await tx.user_settings
|
||||||
where: { id: userId }
|
.delete({
|
||||||
}).catch(() => {});
|
where: { id: userId },
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
await tx.sessions.deleteMany({
|
await tx.sessions.deleteMany({
|
||||||
where: { user: userId }
|
where: { user: userId },
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.users.delete({
|
await tx.users.delete({
|
||||||
where: { id: userId }
|
where: { id: userId },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info('User account deleted successfully', { userId });
|
log.info('User account deleted successfully', { userId });
|
||||||
|
|
||||||
return { success: true, message: 'User account deleted successfully' };
|
return { success: true, message: 'User account deleted successfully' };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Failed to delete user account', {
|
log.error('Failed to delete user account', {
|
||||||
userId,
|
userId,
|
||||||
error: error instanceof Error ? error.message : String(error)
|
error: error instanceof Error ? error.message : String(error),
|
||||||
});
|
});
|
||||||
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
message: 'Failed to delete user account',
|
message: 'Failed to delete user account',
|
||||||
cause: error instanceof Error ? error.message : 'Unknown error'
|
cause: error instanceof Error ? error.message : 'Unknown error',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 405,
|
statusCode: 405,
|
||||||
message: 'Method not allowed'
|
message: 'Method not allowed',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { useAuth } from "#imports";
|
import { useAuth } from '#imports';
|
||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async event => {
|
||||||
const userId = event.context.params?.id;
|
const userId = event.context.params?.id;
|
||||||
const listId = event.context.params?.listId;
|
const listId = event.context.params?.listId;
|
||||||
const session = await useAuth().getCurrentSession();
|
const session = await useAuth().getCurrentSession();
|
||||||
|
|
@ -11,7 +11,7 @@ export default defineEventHandler(async (event) => {
|
||||||
if (session.user !== userId) {
|
if (session.user !== userId) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 403,
|
statusCode: 403,
|
||||||
message: "Cannot delete lists for other users",
|
message: 'Cannot delete lists for other users',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const list = await prisma.lists.findUnique({
|
const list = await prisma.lists.findUnique({
|
||||||
|
|
@ -21,7 +21,7 @@ export default defineEventHandler(async (event) => {
|
||||||
if (!list) {
|
if (!list) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 404,
|
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({
|
await tx.list_items.deleteMany({
|
||||||
where: { list_id: listId },
|
where: { list_id: listId },
|
||||||
});
|
});
|
||||||
|
|
@ -44,6 +44,6 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: listId,
|
id: listId,
|
||||||
message: "List deleted successfully",
|
message: 'List deleted successfully',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { useAuth } from "#imports";
|
import { useAuth } from '#imports';
|
||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async event => {
|
||||||
const userId = event.context.params?.id;
|
const userId = event.context.params?.id;
|
||||||
const session = await useAuth().getCurrentSession();
|
const session = await useAuth().getCurrentSession();
|
||||||
|
|
||||||
if (session.user !== userId) {
|
if (session.user !== userId) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 403,
|
statusCode: 403,
|
||||||
message: "Cannot access other user information",
|
message: 'Cannot access other user information',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { useAuth } from "#imports";
|
import { useAuth } from '#imports';
|
||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { z } from "zod";
|
import { z } from 'zod';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
const listItemSchema = z.object({
|
const listItemSchema = z.object({
|
||||||
tmdb_id: z.string(),
|
tmdb_id: z.string(),
|
||||||
type: z.enum(["movie", "tv"]),
|
type: z.enum(['movie', 'tv']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateListSchema = z.object({
|
const updateListSchema = z.object({
|
||||||
|
|
@ -18,14 +18,14 @@ const updateListSchema = z.object({
|
||||||
removeItems: z.array(listItemSchema).optional(),
|
removeItems: z.array(listItemSchema).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async event => {
|
||||||
const userId = event.context.params?.id;
|
const userId = event.context.params?.id;
|
||||||
const session = await useAuth().getCurrentSession();
|
const session = await useAuth().getCurrentSession();
|
||||||
|
|
||||||
if (session.user !== userId) {
|
if (session.user !== userId) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 403,
|
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) {
|
if (!list) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 404,
|
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 (
|
if (
|
||||||
validatedBody.name ||
|
validatedBody.name ||
|
||||||
validatedBody.description !== undefined ||
|
validatedBody.description !== undefined ||
|
||||||
|
|
@ -62,24 +62,22 @@ export default defineEventHandler(async (event) => {
|
||||||
data: {
|
data: {
|
||||||
name: validatedBody.name ?? list.name,
|
name: validatedBody.name ?? list.name,
|
||||||
description:
|
description:
|
||||||
validatedBody.description !== undefined
|
validatedBody.description !== undefined ? validatedBody.description : list.description,
|
||||||
? validatedBody.description
|
|
||||||
: list.description,
|
|
||||||
public: validatedBody.public ?? list.public,
|
public: validatedBody.public ?? list.public,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validatedBody.addItems && validatedBody.addItems.length > 0) {
|
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(
|
const itemsToAdd = validatedBody.addItems.filter(
|
||||||
(item) => !existingTmdbIds.includes(item.tmdb_id)
|
item => !existingTmdbIds.includes(item.tmdb_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (itemsToAdd.length > 0) {
|
if (itemsToAdd.length > 0) {
|
||||||
await tx.list_items.createMany({
|
await tx.list_items.createMany({
|
||||||
data: itemsToAdd.map((item) => ({
|
data: itemsToAdd.map(item => ({
|
||||||
list_id: list.id,
|
list_id: list.id,
|
||||||
tmdb_id: item.tmdb_id,
|
tmdb_id: item.tmdb_id,
|
||||||
type: item.type,
|
type: item.type,
|
||||||
|
|
@ -90,9 +88,7 @@ export default defineEventHandler(async (event) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validatedBody.removeItems && validatedBody.removeItems.length > 0) {
|
if (validatedBody.removeItems && validatedBody.removeItems.length > 0) {
|
||||||
const tmdbIdsToRemove = validatedBody.removeItems.map(
|
const tmdbIdsToRemove = validatedBody.removeItems.map(item => item.tmdb_id);
|
||||||
(item) => item.tmdb_id
|
|
||||||
);
|
|
||||||
|
|
||||||
await tx.list_items.deleteMany({
|
await tx.list_items.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
|
|
@ -110,6 +106,6 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
list: result,
|
list: result,
|
||||||
message: "List updated successfully",
|
message: 'List updated successfully',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { useAuth } from "#imports";
|
import { useAuth } from '#imports';
|
||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { z } from "zod";
|
import { z } from 'zod';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
const listItemSchema = z.object({
|
const listItemSchema = z.object({
|
||||||
tmdb_id: z.string(),
|
tmdb_id: z.string(),
|
||||||
type: z.enum(["movie", "tv"]),
|
type: z.enum(['movie', 'tv']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const createListSchema = z.object({
|
const createListSchema = z.object({
|
||||||
|
|
@ -16,14 +16,14 @@ const createListSchema = z.object({
|
||||||
public: z.boolean().optional(),
|
public: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async event => {
|
||||||
const userId = event.context.params?.id;
|
const userId = event.context.params?.id;
|
||||||
const session = await useAuth().getCurrentSession();
|
const session = await useAuth().getCurrentSession();
|
||||||
|
|
||||||
if (session.user !== userId) {
|
if (session.user !== userId) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 403,
|
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;
|
let parsedBody;
|
||||||
try {
|
try {
|
||||||
parsedBody = typeof body === "string" ? JSON.parse(body) : body;
|
parsedBody = typeof body === 'string' ? JSON.parse(body) : body;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: "Invalid request body format",
|
message: 'Invalid request body format',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const validatedBody = createListSchema.parse(parsedBody);
|
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({
|
const newList = await tx.lists.create({
|
||||||
data: {
|
data: {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
|
|
@ -53,7 +53,7 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
if (validatedBody.items && validatedBody.items.length > 0) {
|
if (validatedBody.items && validatedBody.items.length > 0) {
|
||||||
await tx.list_items.createMany({
|
await tx.list_items.createMany({
|
||||||
data: validatedBody.items.map((item) => ({
|
data: validatedBody.items.map(item => ({
|
||||||
list_id: newList.id,
|
list_id: newList.id,
|
||||||
tmdb_id: item.tmdb_id,
|
tmdb_id: item.tmdb_id,
|
||||||
type: item.type, // Type is mapped here
|
type: item.type, // Type is mapped here
|
||||||
|
|
@ -70,6 +70,6 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
list: result,
|
list: result,
|
||||||
message: "List created successfully",
|
message: 'List created successfully',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,14 @@ const progressMetaSchema = z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
year: z.number().optional(),
|
year: z.number().optional(),
|
||||||
poster: z.string().optional(),
|
poster: z.string().optional(),
|
||||||
type: z.enum(['movie', 'show'])
|
type: z.enum(['movie', 'show']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const progressItemSchema = z.object({
|
const progressItemSchema = z.object({
|
||||||
meta: progressMetaSchema,
|
meta: progressMetaSchema,
|
||||||
tmdbId: z.string(),
|
tmdbId: z.string(),
|
||||||
duration: z.number().transform((n) => n.toString()),
|
duration: z.number().transform(n => n.toString()),
|
||||||
watched: z.number().transform((n) => n.toString()),
|
watched: z.number().transform(n => n.toString()),
|
||||||
seasonId: z.string().optional(),
|
seasonId: z.string().optional(),
|
||||||
episodeId: z.string().optional(),
|
episodeId: z.string().optional(),
|
||||||
seasonNumber: z.number().optional(),
|
seasonNumber: z.number().optional(),
|
||||||
|
|
@ -30,28 +30,28 @@ function defaultAndCoerceDateTime(dateTime: string | undefined) {
|
||||||
return new Date(clampedEpoch);
|
return new Date(clampedEpoch);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async event => {
|
||||||
const userId = event.context.params?.id;
|
const userId = event.context.params?.id;
|
||||||
const method = event.method;
|
const method = event.method;
|
||||||
|
|
||||||
const session = await useAuth().getCurrentSession();
|
const session = await useAuth().getCurrentSession();
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
message: 'Session not found or expired'
|
message: 'Session not found or expired',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.user !== userId) {
|
if (session.user !== userId) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 403,
|
statusCode: 403,
|
||||||
message: 'Cannot access other user information'
|
message: 'Cannot access other user information',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method === 'GET') {
|
if (method === 'GET') {
|
||||||
const items = await prisma.progress_items.findMany({
|
const items = await prisma.progress_items.findMany({
|
||||||
where: { user_id: userId }
|
where: { user_id: userId },
|
||||||
});
|
});
|
||||||
|
|
||||||
return items.map(item => ({
|
return items.map(item => ({
|
||||||
|
|
@ -59,53 +59,53 @@ export default defineEventHandler(async (event) => {
|
||||||
tmdbId: item.tmdb_id,
|
tmdbId: item.tmdb_id,
|
||||||
episode: {
|
episode: {
|
||||||
id: item.episode_id || null,
|
id: item.episode_id || null,
|
||||||
number: item.episode_number || null
|
number: item.episode_number || null,
|
||||||
},
|
},
|
||||||
season: {
|
season: {
|
||||||
id: item.season_id || null,
|
id: item.season_id || null,
|
||||||
number: item.season_number || null
|
number: item.season_number || null,
|
||||||
},
|
},
|
||||||
meta: item.meta,
|
meta: item.meta,
|
||||||
duration: item.duration.toString(),
|
duration: item.duration.toString(),
|
||||||
watched: item.watched.toString(),
|
watched: item.watched.toString(),
|
||||||
updatedAt: item.updated_at.toISOString()
|
updatedAt: item.updated_at.toISOString(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.path.includes('/progress/') && !event.path.endsWith('/import')) {
|
if (event.path.includes('/progress/') && !event.path.endsWith('/import')) {
|
||||||
const segments = event.path.split('/');
|
const segments = event.path.split('/');
|
||||||
const tmdbId = segments[segments.length - 1];
|
const tmdbId = segments[segments.length - 1];
|
||||||
|
|
||||||
if (method === 'PUT') {
|
if (method === 'PUT') {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
const validatedBody = progressItemSchema.parse(body);
|
const validatedBody = progressItemSchema.parse(body);
|
||||||
|
|
||||||
const now = defaultAndCoerceDateTime(validatedBody.updatedAt);
|
const now = defaultAndCoerceDateTime(validatedBody.updatedAt);
|
||||||
|
|
||||||
const existingItem = await prisma.progress_items.findUnique({
|
const existingItem = await prisma.progress_items.findUnique({
|
||||||
where: {
|
where: {
|
||||||
tmdb_id_user_id_season_id_episode_id: {
|
tmdb_id_user_id_season_id_episode_id: {
|
||||||
tmdb_id: tmdbId,
|
tmdb_id: tmdbId,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
season_id: validatedBody.seasonId || null,
|
season_id: validatedBody.seasonId || null,
|
||||||
episode_id: validatedBody.episodeId || null
|
episode_id: validatedBody.episodeId || null,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let progressItem;
|
let progressItem;
|
||||||
|
|
||||||
if (existingItem) {
|
if (existingItem) {
|
||||||
progressItem = await prisma.progress_items.update({
|
progressItem = await prisma.progress_items.update({
|
||||||
where: {
|
where: {
|
||||||
id: existingItem.id
|
id: existingItem.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
duration: BigInt(validatedBody.duration),
|
duration: BigInt(validatedBody.duration),
|
||||||
watched: BigInt(validatedBody.watched),
|
watched: BigInt(validatedBody.watched),
|
||||||
meta: validatedBody.meta,
|
meta: validatedBody.meta,
|
||||||
updated_at: now
|
updated_at: now,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
progressItem = await prisma.progress_items.create({
|
progressItem = await prisma.progress_items.create({
|
||||||
|
|
@ -120,11 +120,11 @@ export default defineEventHandler(async (event) => {
|
||||||
duration: BigInt(validatedBody.duration),
|
duration: BigInt(validatedBody.duration),
|
||||||
watched: BigInt(validatedBody.watched),
|
watched: BigInt(validatedBody.watched),
|
||||||
meta: validatedBody.meta,
|
meta: validatedBody.meta,
|
||||||
updated_at: now
|
updated_at: now,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: progressItem.id,
|
id: progressItem.id,
|
||||||
tmdbId: progressItem.tmdb_id,
|
tmdbId: progressItem.tmdb_id,
|
||||||
|
|
@ -136,49 +136,49 @@ export default defineEventHandler(async (event) => {
|
||||||
meta: progressItem.meta,
|
meta: progressItem.meta,
|
||||||
duration: Number(progressItem.duration),
|
duration: Number(progressItem.duration),
|
||||||
watched: Number(progressItem.watched),
|
watched: Number(progressItem.watched),
|
||||||
updatedAt: progressItem.updated_at
|
updatedAt: progressItem.updated_at,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method === 'DELETE') {
|
if (method === 'DELETE') {
|
||||||
const body = await readBody(event).catch(() => ({}));
|
const body = await readBody(event).catch(() => ({}));
|
||||||
|
|
||||||
const whereClause: any = {
|
const whereClause: any = {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
tmdb_id: tmdbId
|
tmdb_id: tmdbId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (body.seasonId) whereClause.season_id = body.seasonId;
|
if (body.seasonId) whereClause.season_id = body.seasonId;
|
||||||
if (body.episodeId) whereClause.episode_id = body.episodeId;
|
if (body.episodeId) whereClause.episode_id = body.episodeId;
|
||||||
|
|
||||||
const itemsToDelete = await prisma.progress_items.findMany({
|
const itemsToDelete = await prisma.progress_items.findMany({
|
||||||
where: whereClause
|
where: whereClause,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (itemsToDelete.length === 0) {
|
if (itemsToDelete.length === 0) {
|
||||||
return {
|
return {
|
||||||
count: 0,
|
count: 0,
|
||||||
tmdbId,
|
tmdbId,
|
||||||
episodeId: body.episodeId,
|
episodeId: body.episodeId,
|
||||||
seasonId: body.seasonId
|
seasonId: body.seasonId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.progress_items.deleteMany({
|
await prisma.progress_items.deleteMany({
|
||||||
where: whereClause
|
where: whereClause,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
count: itemsToDelete.length,
|
count: itemsToDelete.length,
|
||||||
tmdbId,
|
tmdbId,
|
||||||
episodeId: body.episodeId,
|
episodeId: body.episodeId,
|
||||||
seasonId: body.seasonId
|
seasonId: body.seasonId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 405,
|
statusCode: 405,
|
||||||
message: 'Method not allowed'
|
message: 'Method not allowed',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,14 @@ const progressMetaSchema = z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
poster: z.string().optional(),
|
poster: z.string().optional(),
|
||||||
type: z.enum(['movie', 'tv', 'show']),
|
type: z.enum(['movie', 'tv', 'show']),
|
||||||
year: z.number().optional()
|
year: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const progressItemSchema = z.object({
|
const progressItemSchema = z.object({
|
||||||
meta: progressMetaSchema,
|
meta: progressMetaSchema,
|
||||||
tmdbId: z.string(),
|
tmdbId: z.string(),
|
||||||
duration: z.number().transform((n) => Math.round(n)),
|
duration: z.number().transform(n => Math.round(n)),
|
||||||
watched: z.number().transform((n) => Math.round(n)),
|
watched: z.number().transform(n => Math.round(n)),
|
||||||
seasonId: z.string().optional(),
|
seasonId: z.string().optional(),
|
||||||
episodeId: z.string().optional(),
|
episodeId: z.string().optional(),
|
||||||
seasonNumber: z.number().optional(),
|
seasonNumber: z.number().optional(),
|
||||||
|
|
@ -30,54 +30,54 @@ function defaultAndCoerceDateTime(dateTime: string | undefined) {
|
||||||
return new Date(clampedEpoch);
|
return new Date(clampedEpoch);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async event => {
|
||||||
const userId = event.context.params?.id;
|
const userId = event.context.params?.id;
|
||||||
const tmdbId = event.context.params?.tmdb_id;
|
const tmdbId = event.context.params?.tmdb_id;
|
||||||
const method = event.method;
|
const method = event.method;
|
||||||
|
|
||||||
const session = await useAuth().getCurrentSession();
|
const session = await useAuth().getCurrentSession();
|
||||||
|
|
||||||
if (session.user !== userId) {
|
if (session.user !== userId) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 403,
|
statusCode: 403,
|
||||||
message: 'Different userId than authenticated session'
|
message: 'Different userId than authenticated session',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method === 'PUT') {
|
if (method === 'PUT') {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
const validatedBody = progressItemSchema.parse(body);
|
const validatedBody = progressItemSchema.parse(body);
|
||||||
|
|
||||||
const now = defaultAndCoerceDateTime(validatedBody.updatedAt);
|
const now = defaultAndCoerceDateTime(validatedBody.updatedAt);
|
||||||
|
|
||||||
const isMovie = validatedBody.meta.type === 'movie';
|
const isMovie = validatedBody.meta.type === 'movie';
|
||||||
const seasonId = isMovie ? '\n' : (validatedBody.seasonId || null);
|
const seasonId = isMovie ? '\n' : validatedBody.seasonId || null;
|
||||||
const episodeId = isMovie ? '\n' : (validatedBody.episodeId || null);
|
const episodeId = isMovie ? '\n' : validatedBody.episodeId || null;
|
||||||
|
|
||||||
const existingItem = await prisma.progress_items.findUnique({
|
const existingItem = await prisma.progress_items.findUnique({
|
||||||
where: {
|
where: {
|
||||||
tmdb_id_user_id_season_id_episode_id: {
|
tmdb_id_user_id_season_id_episode_id: {
|
||||||
tmdb_id: tmdbId,
|
tmdb_id: tmdbId,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
season_id: seasonId,
|
season_id: seasonId,
|
||||||
episode_id: episodeId
|
episode_id: episodeId,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let progressItem;
|
let progressItem;
|
||||||
|
|
||||||
if (existingItem) {
|
if (existingItem) {
|
||||||
progressItem = await prisma.progress_items.update({
|
progressItem = await prisma.progress_items.update({
|
||||||
where: {
|
where: {
|
||||||
id: existingItem.id
|
id: existingItem.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
duration: BigInt(validatedBody.duration),
|
duration: BigInt(validatedBody.duration),
|
||||||
watched: BigInt(validatedBody.watched),
|
watched: BigInt(validatedBody.watched),
|
||||||
meta: validatedBody.meta,
|
meta: validatedBody.meta,
|
||||||
updated_at: now
|
updated_at: now,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
progressItem = await prisma.progress_items.create({
|
progressItem = await prisma.progress_items.create({
|
||||||
|
|
@ -92,11 +92,11 @@ export default defineEventHandler(async (event) => {
|
||||||
duration: BigInt(validatedBody.duration),
|
duration: BigInt(validatedBody.duration),
|
||||||
watched: BigInt(validatedBody.watched),
|
watched: BigInt(validatedBody.watched),
|
||||||
meta: validatedBody.meta,
|
meta: validatedBody.meta,
|
||||||
updated_at: now
|
updated_at: now,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: progressItem.id,
|
id: progressItem.id,
|
||||||
tmdbId: progressItem.tmdb_id,
|
tmdbId: progressItem.tmdb_id,
|
||||||
|
|
@ -108,55 +108,55 @@ export default defineEventHandler(async (event) => {
|
||||||
meta: progressItem.meta,
|
meta: progressItem.meta,
|
||||||
duration: Number(progressItem.duration),
|
duration: Number(progressItem.duration),
|
||||||
watched: Number(progressItem.watched),
|
watched: Number(progressItem.watched),
|
||||||
updatedAt: progressItem.updated_at
|
updatedAt: progressItem.updated_at,
|
||||||
};
|
};
|
||||||
} else if (method === 'DELETE') {
|
} else if (method === 'DELETE') {
|
||||||
const body = await readBody(event).catch(() => ({}));
|
const body = await readBody(event).catch(() => ({}));
|
||||||
|
|
||||||
const whereClause: any = {
|
const whereClause: any = {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
tmdb_id: tmdbId
|
tmdb_id: tmdbId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (body.seasonId) {
|
if (body.seasonId) {
|
||||||
whereClause.season_id = body.seasonId;
|
whereClause.season_id = body.seasonId;
|
||||||
} else if (body.meta?.type === 'movie') {
|
} else if (body.meta?.type === 'movie') {
|
||||||
whereClause.season_id = '\n';
|
whereClause.season_id = '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body.episodeId) {
|
if (body.episodeId) {
|
||||||
whereClause.episode_id = body.episodeId;
|
whereClause.episode_id = body.episodeId;
|
||||||
} else if (body.meta?.type === 'movie') {
|
} else if (body.meta?.type === 'movie') {
|
||||||
whereClause.episode_id = '\n';
|
whereClause.episode_id = '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemsToDelete = await prisma.progress_items.findMany({
|
const itemsToDelete = await prisma.progress_items.findMany({
|
||||||
where: whereClause
|
where: whereClause,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (itemsToDelete.length === 0) {
|
if (itemsToDelete.length === 0) {
|
||||||
return {
|
return {
|
||||||
count: 0,
|
count: 0,
|
||||||
tmdbId,
|
tmdbId,
|
||||||
episodeId: body.episodeId,
|
episodeId: body.episodeId,
|
||||||
seasonId: body.seasonId
|
seasonId: body.seasonId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.progress_items.deleteMany({
|
await prisma.progress_items.deleteMany({
|
||||||
where: whereClause
|
where: whereClause,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
count: itemsToDelete.length,
|
count: itemsToDelete.length,
|
||||||
tmdbId,
|
tmdbId,
|
||||||
episodeId: body.episodeId,
|
episodeId: body.episodeId,
|
||||||
seasonId: body.seasonId
|
seasonId: body.seasonId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 405,
|
statusCode: 405,
|
||||||
message: 'Method not allowed'
|
message: 'Method not allowed',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,19 +9,19 @@ const progressMetaSchema = z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
type: z.enum(['movie', 'show']),
|
type: z.enum(['movie', 'show']),
|
||||||
year: z.number(),
|
year: z.number(),
|
||||||
poster: z.string().optional()
|
poster: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const progressItemSchema = z.object({
|
const progressItemSchema = z.object({
|
||||||
meta: progressMetaSchema,
|
meta: progressMetaSchema,
|
||||||
tmdbId: z.string(),
|
tmdbId: z.string(),
|
||||||
duration: z.number(),
|
duration: z.number().min(0),
|
||||||
watched: z.number(),
|
watched: z.number().min(0),
|
||||||
seasonId: z.string().optional(),
|
seasonId: z.string().optional(),
|
||||||
episodeId: z.string().optional(),
|
episodeId: z.string().optional(),
|
||||||
seasonNumber: z.number().optional(),
|
seasonNumber: z.number().optional(),
|
||||||
episodeNumber: 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
|
// 13th July 2021 - movie-web epoch
|
||||||
|
|
@ -33,47 +33,59 @@ function defaultAndCoerceDateTime(dateTime: string | undefined) {
|
||||||
return new Date(clampedEpoch);
|
return new Date(clampedEpoch);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async event => {
|
||||||
const userId = event.context.params?.id;
|
const userId = event.context.params?.id;
|
||||||
|
|
||||||
const session = await useAuth().getCurrentSession();
|
const session = await useAuth().getCurrentSession();
|
||||||
|
|
||||||
if (session.user !== userId) {
|
if (session.user !== userId) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 403,
|
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') {
|
if (event.method !== 'PUT') {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 405,
|
statusCode: 405,
|
||||||
message: 'Method not allowed'
|
message: 'Method not allowed',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
const validatedBody = z.array(progressItemSchema).parse(body);
|
const validatedBody = z.array(progressItemSchema).parse(body);
|
||||||
|
|
||||||
const existingItems = await prisma.progress_items.findMany({
|
const existingItems = await prisma.progress_items.findMany({
|
||||||
where: { user_id: userId }
|
where: { user_id: userId },
|
||||||
});
|
});
|
||||||
|
|
||||||
const newItems = [...validatedBody];
|
const newItems = [...validatedBody];
|
||||||
const itemsToUpsert = [];
|
const itemsToUpsert = [];
|
||||||
|
|
||||||
for (const existingItem of existingItems) {
|
for (const existingItem of existingItems) {
|
||||||
const newItemIndex = newItems.findIndex(
|
const newItemIndex = newItems.findIndex(
|
||||||
(item) =>
|
item =>
|
||||||
item.tmdbId === existingItem.tmdb_id &&
|
item.tmdbId === existingItem.tmdb_id &&
|
||||||
item.seasonId === (existingItem.season_id === '\n' ? null : existingItem.season_id) &&
|
item.seasonId === (existingItem.season_id === '\n' ? null : existingItem.season_id) &&
|
||||||
item.episodeId === (existingItem.episode_id === '\n' ? null : existingItem.episode_id)
|
item.episodeId === (existingItem.episode_id === '\n' ? null : existingItem.episode_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newItemIndex > -1) {
|
if (newItemIndex > -1) {
|
||||||
const newItem = newItems[newItemIndex];
|
const newItem = newItems[newItemIndex];
|
||||||
|
|
||||||
if (Number(existingItem.watched) < newItem.watched) {
|
if (Number(existingItem.watched) < newItem.watched) {
|
||||||
const isMovie = newItem.meta.type === 'movie';
|
const isMovie = newItem.meta.type === 'movie';
|
||||||
itemsToUpsert.push({
|
itemsToUpsert.push({
|
||||||
|
|
@ -87,14 +99,14 @@ export default defineEventHandler(async (event) => {
|
||||||
duration: BigInt(newItem.duration),
|
duration: BigInt(newItem.duration),
|
||||||
watched: BigInt(newItem.watched),
|
watched: BigInt(newItem.watched),
|
||||||
meta: newItem.meta,
|
meta: newItem.meta,
|
||||||
updated_at: defaultAndCoerceDateTime(newItem.updatedAt)
|
updated_at: defaultAndCoerceDateTime(newItem.updatedAt),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
newItems.splice(newItemIndex, 1);
|
newItems.splice(newItemIndex, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new items
|
// Create new items
|
||||||
for (const item of newItems) {
|
for (const item of newItems) {
|
||||||
const isMovie = item.meta.type === 'movie';
|
const isMovie = item.meta.type === 'movie';
|
||||||
|
|
@ -102,17 +114,17 @@ export default defineEventHandler(async (event) => {
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
tmdb_id: item.tmdbId,
|
tmdb_id: item.tmdbId,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
season_id: isMovie ? '\n' : (item.seasonId || null),
|
season_id: isMovie ? '\n' : item.seasonId || null,
|
||||||
episode_id: isMovie ? '\n' : (item.episodeId || null),
|
episode_id: isMovie ? '\n' : item.episodeId || null,
|
||||||
season_number: isMovie ? null : item.seasonNumber,
|
season_number: isMovie ? null : item.seasonNumber,
|
||||||
episode_number: isMovie ? null : item.episodeNumber,
|
episode_number: isMovie ? null : item.episodeNumber,
|
||||||
duration: BigInt(item.duration),
|
duration: BigInt(item.duration),
|
||||||
watched: BigInt(item.watched),
|
watched: BigInt(item.watched),
|
||||||
meta: item.meta,
|
meta: item.meta,
|
||||||
updated_at: defaultAndCoerceDateTime(item.updatedAt)
|
updated_at: defaultAndCoerceDateTime(item.updatedAt),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upsert all items
|
// Upsert all items
|
||||||
const results = [];
|
const results = [];
|
||||||
for (const item of itemsToUpsert) {
|
for (const item of itemsToUpsert) {
|
||||||
|
|
@ -123,59 +135,63 @@ export default defineEventHandler(async (event) => {
|
||||||
tmdb_id: item.tmdb_id,
|
tmdb_id: item.tmdb_id,
|
||||||
user_id: item.user_id,
|
user_id: item.user_id,
|
||||||
season_id: item.season_id,
|
season_id: item.season_id,
|
||||||
episode_id: item.episode_id
|
episode_id: item.episode_id,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
create: item,
|
create: item,
|
||||||
update: {
|
update: {
|
||||||
duration: item.duration,
|
duration: item.duration,
|
||||||
watched: item.watched,
|
watched: item.watched,
|
||||||
meta: item.meta,
|
meta: item.meta,
|
||||||
updated_at: item.updated_at
|
updated_at: item.updated_at,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
id: result.id,
|
id: result.id,
|
||||||
tmdbId: result.tmdb_id,
|
tmdbId: result.tmdb_id,
|
||||||
episode: {
|
episode: {
|
||||||
id: result.episode_id === '\n' ? null : result.episode_id,
|
id: result.episode_id === '\n' ? null : result.episode_id,
|
||||||
number: result.episode_number
|
number: result.episode_number,
|
||||||
},
|
},
|
||||||
season: {
|
season: {
|
||||||
id: result.season_id === '\n' ? null : result.season_id,
|
id: result.season_id === '\n' ? null : result.season_id,
|
||||||
number: result.season_number
|
number: result.season_number,
|
||||||
},
|
},
|
||||||
meta: result.meta,
|
meta: result.meta,
|
||||||
duration: result.duration.toString(),
|
duration: result.duration.toString(),
|
||||||
watched: result.watched.toString(),
|
watched: result.watched.toString(),
|
||||||
updatedAt: result.updated_at.toISOString()
|
updatedAt: result.updated_at.toISOString(),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Failed to upsert progress item', {
|
log.error('Failed to upsert progress item', {
|
||||||
userId,
|
userId,
|
||||||
tmdbId: item.tmdb_id,
|
tmdbId: item.tmdb_id,
|
||||||
error: error instanceof Error ? error.message : String(error)
|
error: error instanceof Error ? error.message : String(error),
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Failed to import progress', {
|
log.error('Failed to import progress', {
|
||||||
userId,
|
userId,
|
||||||
error: error instanceof Error ? error.message : String(error)
|
error: error instanceof Error ? error.message : String(error),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: 'Invalid progress data',
|
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',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,86 +2,87 @@ import { useAuth } from '~/utils/auth';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const userRatingsSchema = z.object({
|
const userRatingsSchema = z.object({
|
||||||
tmdb_id: z.number(),
|
tmdb_id: z.number(),
|
||||||
type: z.enum(['movie', 'tv']),
|
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 userId = event.context.params?.id;
|
||||||
|
|
||||||
const session = await useAuth().getCurrentSession();
|
|
||||||
|
|
||||||
if (session.user !== userId) {
|
const session = await useAuth().getCurrentSession();
|
||||||
throw createError({
|
|
||||||
statusCode: 403,
|
|
||||||
message: 'Permission denied'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.method === 'GET'){
|
if (session.user !== userId) {
|
||||||
const ratings = await prisma.users.findMany({
|
|
||||||
select: {
|
|
||||||
ratings: true
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: userId
|
|
||||||
}});
|
|
||||||
|
|
||||||
return {
|
|
||||||
userId,
|
|
||||||
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
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
ratings: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const userRatings = user?.ratings || [];
|
|
||||||
const currentRatings = Array.isArray(userRatings) ? userRatings : [];
|
|
||||||
|
|
||||||
const existingRatingIndex = currentRatings.findIndex(
|
|
||||||
(r: any) => r.tmdb_id === validatedBody.tmdb_id && r.type === validatedBody.type
|
|
||||||
);
|
|
||||||
|
|
||||||
let updatedRatings;
|
|
||||||
if (existingRatingIndex >= 0) {
|
|
||||||
updatedRatings = [...currentRatings];
|
|
||||||
updatedRatings[existingRatingIndex] = validatedBody;
|
|
||||||
} else {
|
|
||||||
updatedRatings = [...currentRatings, validatedBody];
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.users.update({
|
|
||||||
where: {
|
|
||||||
id: userId
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
ratings: updatedRatings
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
userId,
|
|
||||||
rating: {
|
|
||||||
tmdb_id: validatedBody.tmdb_id,
|
|
||||||
type: validatedBody.type,
|
|
||||||
rating: validatedBody.rating
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should only execute if the method is neither GET nor POST
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 405,
|
statusCode: 403,
|
||||||
message: 'Method not allowed'
|
message: 'Permission denied',
|
||||||
});
|
});
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if (event.method === 'GET') {
|
||||||
|
const ratings = await prisma.users.findMany({
|
||||||
|
select: {
|
||||||
|
ratings: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId,
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
ratings: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const userRatings = user?.ratings || [];
|
||||||
|
const currentRatings = Array.isArray(userRatings) ? userRatings : [];
|
||||||
|
|
||||||
|
const existingRatingIndex = currentRatings.findIndex(
|
||||||
|
(r: any) => r.tmdb_id === validatedBody.tmdb_id && r.type === validatedBody.type
|
||||||
|
);
|
||||||
|
|
||||||
|
let updatedRatings;
|
||||||
|
if (existingRatingIndex >= 0) {
|
||||||
|
updatedRatings = [...currentRatings];
|
||||||
|
updatedRatings[existingRatingIndex] = validatedBody;
|
||||||
|
} else {
|
||||||
|
updatedRatings = [...currentRatings, validatedBody];
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.users.update({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
ratings: updatedRatings,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId,
|
||||||
|
rating: {
|
||||||
|
tmdb_id: validatedBody.tmdb_id,
|
||||||
|
type: validatedBody.type,
|
||||||
|
rating: validatedBody.rating,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should only execute if the method is neither GET nor POST
|
||||||
|
throw createError({
|
||||||
|
statusCode: 405,
|
||||||
|
message: 'Method not allowed',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
export default defineEventHandler(() => {
|
export default defineEventHandler(() => {
|
||||||
|
return;
|
||||||
|
});
|
||||||
return
|
|
||||||
})
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
import { useAuth } from '~/utils/auth';
|
import { useAuth } from '~/utils/auth';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async event => {
|
||||||
const userId = getRouterParam(event, 'id');
|
const userId = getRouterParam(event, 'id');
|
||||||
|
|
||||||
const session = await useAuth().getCurrentSession();
|
const session = await useAuth().getCurrentSession();
|
||||||
|
|
||||||
if (session.user !== userId) {
|
if (session.user !== userId) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 403,
|
statusCode: 403,
|
||||||
message: 'Cannot access sessions for other users'
|
message: 'Cannot access sessions for other users',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessions = await prisma.sessions.findMany({
|
const sessions = await prisma.sessions.findMany({
|
||||||
where: { user: userId }
|
where: { user: userId },
|
||||||
});
|
});
|
||||||
|
|
||||||
return sessions.map(s => ({
|
return sessions.map(s => ({
|
||||||
|
|
@ -22,6 +22,6 @@ export default defineEventHandler(async (event) => {
|
||||||
createdAt: s.created_at.toISOString(),
|
createdAt: s.created_at.toISOString(),
|
||||||
accessedAt: s.accessed_at.toISOString(),
|
accessedAt: s.accessed_at.toISOString(),
|
||||||
device: s.device,
|
device: s.device,
|
||||||
userAgent: s.user_agent
|
userAgent: s.user_agent,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -10,51 +10,74 @@ const userSettingsSchema = z.object({
|
||||||
defaultSubtitleLanguage: z.string().nullable().optional(),
|
defaultSubtitleLanguage: z.string().nullable().optional(),
|
||||||
proxyUrls: z.array(z.string()).nullable().optional(),
|
proxyUrls: z.array(z.string()).nullable().optional(),
|
||||||
traktKey: 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 userId = event.context.params?.id;
|
||||||
|
|
||||||
const session = await useAuth().getCurrentSession();
|
const session = await useAuth().getCurrentSession();
|
||||||
|
|
||||||
if (session.user !== userId) {
|
if (session.user !== userId) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 403,
|
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') {
|
if (event.method === 'GET') {
|
||||||
const settings = await prisma.user_settings.findUnique({
|
try {
|
||||||
where: { id: userId }
|
const settings = await prisma.user_settings.findUnique({
|
||||||
});
|
where: { id: userId },
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: userId,
|
id: userId,
|
||||||
applicationTheme: settings?.application_theme || null,
|
applicationTheme: settings?.application_theme || null,
|
||||||
applicationLanguage: settings?.application_language || 'en',
|
applicationLanguage: settings?.application_language || 'en',
|
||||||
defaultSubtitleLanguage: settings?.default_subtitle_language || null,
|
defaultSubtitleLanguage: settings?.default_subtitle_language || null,
|
||||||
proxyUrls: settings?.proxy_urls.length === 0 ? null : settings?.proxy_urls || null,
|
proxyUrls: settings?.proxy_urls.length === 0 ? null : settings?.proxy_urls || null,
|
||||||
traktKey: settings?.trakt_key || 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') {
|
if (event.method === 'PUT') {
|
||||||
try {
|
try {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
log.info('Updating user settings', { userId, body });
|
log.info('Updating user settings', { userId, body });
|
||||||
|
|
||||||
const validatedBody = userSettingsSchema.parse(body);
|
const validatedBody = userSettingsSchema.parse(body);
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
application_theme: validatedBody.applicationTheme ?? null,
|
application_theme: validatedBody.applicationTheme ?? null,
|
||||||
application_language: validatedBody.applicationLanguage,
|
application_language: validatedBody.applicationLanguage,
|
||||||
default_subtitle_language: validatedBody.defaultSubtitleLanguage ?? null,
|
default_subtitle_language: validatedBody.defaultSubtitleLanguage ?? null,
|
||||||
proxy_urls: validatedBody.proxyUrls === null ? [] : validatedBody.proxyUrls || [],
|
proxy_urls: validatedBody.proxyUrls === null ? [] : validatedBody.proxyUrls || [],
|
||||||
trakt_key: validatedBody.traktKey ?? null,
|
trakt_key: validatedBody.traktKey ?? null,
|
||||||
febbox_key: validatedBody.febboxKey ?? null
|
febbox_key: validatedBody.febboxKey ?? null,
|
||||||
};
|
};
|
||||||
|
|
||||||
log.info('Preparing to upsert settings', { userId, data });
|
log.info('Preparing to upsert settings', { userId, data });
|
||||||
|
|
@ -64,12 +87,12 @@ export default defineEventHandler(async (event) => {
|
||||||
update: data,
|
update: data,
|
||||||
create: {
|
create: {
|
||||||
id: userId,
|
id: userId,
|
||||||
...data
|
...data,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info('Settings updated successfully', { userId });
|
log.info('Settings updated successfully', { userId });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: userId,
|
id: userId,
|
||||||
applicationTheme: settings.application_theme,
|
applicationTheme: settings.application_theme,
|
||||||
|
|
@ -77,39 +100,32 @@ export default defineEventHandler(async (event) => {
|
||||||
defaultSubtitleLanguage: settings.default_subtitle_language,
|
defaultSubtitleLanguage: settings.default_subtitle_language,
|
||||||
proxyUrls: settings.proxy_urls.length === 0 ? null : settings.proxy_urls,
|
proxyUrls: settings.proxy_urls.length === 0 ? null : settings.proxy_urls,
|
||||||
traktKey: settings.trakt_key,
|
traktKey: settings.trakt_key,
|
||||||
febboxKey: settings.febbox_key
|
febboxKey: settings.febbox_key,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
log.error('Failed to update user settings', {
|
||||||
|
userId,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
log.error('Validation error in settings update', {
|
|
||||||
userId,
|
|
||||||
errors: error.errors
|
|
||||||
});
|
|
||||||
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: 'Invalid settings data',
|
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({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
message: 'Failed to update settings',
|
message: 'Failed to update settings',
|
||||||
cause: error instanceof Error ? error.message : 'Unknown error'
|
cause: error instanceof Error ? error.message : 'Unknown error',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 405,
|
statusCode: 405,
|
||||||
message: 'Method not allowed'
|
message: 'Method not allowed',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
import { prisma } from '~/utils/prisma'
|
import { prisma } from '~/utils/prisma';
|
||||||
|
|
|
||||||
|
|
@ -9,37 +9,37 @@ const SESSION_EXPIRY_MS = 21 * 24 * 60 * 60 * 1000;
|
||||||
export function useAuth() {
|
export function useAuth() {
|
||||||
const getSession = async (id: string) => {
|
const getSession = async (id: string) => {
|
||||||
const session = await prisma.sessions.findUnique({
|
const session = await prisma.sessions.findUnique({
|
||||||
where: { id }
|
where: { id },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!session) return null;
|
if (!session) return null;
|
||||||
if (new Date(session.expires_at) < new Date()) return null;
|
if (new Date(session.expires_at) < new Date()) return null;
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSessionAndBump = async (id: string) => {
|
const getSessionAndBump = async (id: string) => {
|
||||||
const session = await getSession(id);
|
const session = await getSession(id);
|
||||||
if (!session) return null;
|
if (!session) return null;
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const expiryDate = new Date(now.getTime() + SESSION_EXPIRY_MS);
|
const expiryDate = new Date(now.getTime() + SESSION_EXPIRY_MS);
|
||||||
|
|
||||||
return await prisma.sessions.update({
|
return await prisma.sessions.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
accessed_at: now,
|
accessed_at: now,
|
||||||
expires_at: expiryDate
|
expires_at: expiryDate,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeSession = async (user: string, device: string, userAgent?: string) => {
|
const makeSession = async (user: string, device: string, userAgent?: string) => {
|
||||||
if (!userAgent) throw new Error('No useragent provided');
|
if (!userAgent) throw new Error('No useragent provided');
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const expiryDate = new Date(now.getTime() + SESSION_EXPIRY_MS);
|
const expiryDate = new Date(now.getTime() + SESSION_EXPIRY_MS);
|
||||||
|
|
||||||
return await prisma.sessions.create({
|
return await prisma.sessions.create({
|
||||||
data: {
|
data: {
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
|
|
@ -48,15 +48,15 @@ export function useAuth() {
|
||||||
user_agent: userAgent,
|
user_agent: userAgent,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
accessed_at: now,
|
accessed_at: now,
|
||||||
expires_at: expiryDate
|
expires_at: expiryDate,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeSessionToken = (session: { id: string }) => {
|
const makeSessionToken = (session: { id: string }) => {
|
||||||
const runtimeConfig = useRuntimeConfig();
|
const runtimeConfig = useRuntimeConfig();
|
||||||
return sign({ sid: session.id }, runtimeConfig.cryptoSecret, {
|
return sign({ sid: session.id }, runtimeConfig.cryptoSecret, {
|
||||||
algorithm: 'HS256'
|
algorithm: 'HS256',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -64,23 +64,23 @@ export function useAuth() {
|
||||||
try {
|
try {
|
||||||
const runtimeConfig = useRuntimeConfig();
|
const runtimeConfig = useRuntimeConfig();
|
||||||
const payload = verify(token, runtimeConfig.cryptoSecret, {
|
const payload = verify(token, runtimeConfig.cryptoSecret, {
|
||||||
algorithms: ['HS256']
|
algorithms: ['HS256'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (typeof payload === 'string') return null;
|
if (typeof payload === 'string') return null;
|
||||||
return payload as { sid: string };
|
return payload as { sid: string };
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCurrentSession = async () => {
|
const getCurrentSession = async () => {
|
||||||
const event = useEvent();
|
const event = useEvent();
|
||||||
const authHeader = getRequestHeader(event, 'authorization');
|
const authHeader = getRequestHeader(event, 'authorization');
|
||||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
message: 'Unauthorized'
|
message: 'Unauthorized',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,7 +89,7 @@ export function useAuth() {
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
message: 'Invalid token'
|
message: 'Invalid token',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,10 +97,10 @@ export function useAuth() {
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
message: 'Session not found or expired'
|
message: 'Session not found or expired',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -110,6 +110,6 @@ export function useAuth() {
|
||||||
makeSession,
|
makeSession,
|
||||||
makeSessionToken,
|
makeSessionToken,
|
||||||
verifySessionToken,
|
verifySessionToken,
|
||||||
getCurrentSession
|
getCurrentSession,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,15 @@ export function useChallenge() {
|
||||||
const createChallengeCode = async (flow: string, authType: string) => {
|
const createChallengeCode = async (flow: string, authType: string) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const expiryDate = new Date(now.getTime() + CHALLENGE_EXPIRY_MS);
|
const expiryDate = new Date(now.getTime() + CHALLENGE_EXPIRY_MS);
|
||||||
|
|
||||||
return await prisma.challenge_codes.create({
|
return await prisma.challenge_codes.create({
|
||||||
data: {
|
data: {
|
||||||
code: randomUUID(),
|
code: randomUUID(),
|
||||||
flow,
|
flow,
|
||||||
auth_type: authType,
|
auth_type: authType,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
expires_at: expiryDate
|
expires_at: expiryDate,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -29,7 +29,7 @@ export function useChallenge() {
|
||||||
authType: string
|
authType: string
|
||||||
) => {
|
) => {
|
||||||
const challengeCode = await prisma.challenge_codes.findUnique({
|
const challengeCode = await prisma.challenge_codes.findUnique({
|
||||||
where: { code }
|
where: { code },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!challengeCode) {
|
if (!challengeCode) {
|
||||||
|
|
@ -50,7 +50,7 @@ export function useChallenge() {
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.challenge_codes.delete({
|
await prisma.challenge_codes.delete({
|
||||||
where: { code }
|
where: { code },
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -62,21 +62,17 @@ export function useChallenge() {
|
||||||
while (normalizedSignature.length % 4 !== 0) {
|
while (normalizedSignature.length % 4 !== 0) {
|
||||||
normalizedSignature += '=';
|
normalizedSignature += '=';
|
||||||
}
|
}
|
||||||
|
|
||||||
let normalizedPublicKey = publicKey.replace(/-/g, '+').replace(/_/g, '/');
|
let normalizedPublicKey = publicKey.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
while (normalizedPublicKey.length % 4 !== 0) {
|
while (normalizedPublicKey.length % 4 !== 0) {
|
||||||
normalizedPublicKey += '=';
|
normalizedPublicKey += '=';
|
||||||
}
|
}
|
||||||
|
|
||||||
const signatureBuffer = Buffer.from(normalizedSignature, 'base64');
|
const signatureBuffer = Buffer.from(normalizedSignature, 'base64');
|
||||||
const publicKeyBuffer = Buffer.from(normalizedPublicKey, 'base64');
|
const publicKeyBuffer = Buffer.from(normalizedPublicKey, 'base64');
|
||||||
const messageBuffer = Buffer.from(data);
|
const messageBuffer = Buffer.from(data);
|
||||||
|
|
||||||
return nacl.sign.detached.verify(
|
return nacl.sign.detached.verify(messageBuffer, signatureBuffer, publicKeyBuffer);
|
||||||
messageBuffer,
|
|
||||||
signatureBuffer,
|
|
||||||
publicKeyBuffer
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Signature verification error:', error);
|
console.error('Signature verification error:', error);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -85,6 +81,6 @@ export function useChallenge() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createChallengeCode,
|
createChallengeCode,
|
||||||
verifyChallengeCode
|
verifyChallengeCode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
export const version = '2.0.0'
|
export const version = '2.0.0';
|
||||||
|
|
|
||||||
|
|
@ -38,4 +38,4 @@ function createLogger(scope: string): Logger {
|
||||||
|
|
||||||
export function scopedLogger(scope: string): Logger {
|
export function scopedLogger(scope: string): Logger {
|
||||||
return createLogger(scope);
|
return createLogger(scope);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,23 +87,19 @@ async function createMetrics(): Promise<Metrics> {
|
||||||
async function saveMetricsToFile() {
|
async function saveMetricsToFile() {
|
||||||
try {
|
try {
|
||||||
if (!metrics) return;
|
if (!metrics) return;
|
||||||
|
|
||||||
const metricsData = await register.getMetricsAsJSON();
|
const metricsData = await register.getMetricsAsJSON();
|
||||||
const relevantMetrics = metricsData.filter(metric =>
|
const relevantMetrics = metricsData.filter(
|
||||||
metric.name.startsWith('mw_') ||
|
metric => metric.name.startsWith('mw_') || metric.name === 'http_request_duration_seconds'
|
||||||
metric.name === 'http_request_duration_seconds'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(METRICS_FILE, JSON.stringify(relevantMetrics, null, 2));
|
||||||
METRICS_FILE,
|
|
||||||
JSON.stringify(relevantMetrics, null, 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
log.info('Metrics saved to file', { evt: 'metrics_saved' });
|
log.info('Metrics saved to file', { evt: 'metrics_saved' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Failed to save metrics', {
|
log.error('Failed to save metrics', {
|
||||||
evt: 'save_metrics_error',
|
evt: 'save_metrics_error',
|
||||||
error: error instanceof Error ? error.message : String(error)
|
error: error instanceof Error ? error.message : String(error),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -117,15 +113,15 @@ async function loadMetricsFromFile(): Promise<any[]> {
|
||||||
|
|
||||||
const data = fs.readFileSync(METRICS_FILE, 'utf8');
|
const data = fs.readFileSync(METRICS_FILE, 'utf8');
|
||||||
const savedMetrics = JSON.parse(data);
|
const savedMetrics = JSON.parse(data);
|
||||||
log.info('Loaded saved metrics', {
|
log.info('Loaded saved metrics', {
|
||||||
evt: 'metrics_loaded',
|
evt: 'metrics_loaded',
|
||||||
count: savedMetrics.length
|
count: savedMetrics.length,
|
||||||
});
|
});
|
||||||
return savedMetrics;
|
return savedMetrics;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Failed to load metrics', {
|
log.error('Failed to load metrics', {
|
||||||
evt: 'load_metrics_error',
|
evt: 'load_metrics_error',
|
||||||
error: error instanceof Error ? error.message : String(error)
|
error: error instanceof Error ? error.message : String(error),
|
||||||
});
|
});
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
@ -172,9 +168,9 @@ export async function setupMetrics() {
|
||||||
const savedMetrics = await loadMetricsFromFile();
|
const savedMetrics = await loadMetricsFromFile();
|
||||||
if (savedMetrics.length > 0) {
|
if (savedMetrics.length > 0) {
|
||||||
log.info('Restoring saved metrics...', { evt: 'restore_metrics' });
|
log.info('Restoring saved metrics...', { evt: 'restore_metrics' });
|
||||||
savedMetrics.forEach((metric) => {
|
savedMetrics.forEach(metric => {
|
||||||
if (metric.values) {
|
if (metric.values) {
|
||||||
metric.values.forEach((value) => {
|
metric.values.forEach(value => {
|
||||||
switch (metric.name) {
|
switch (metric.name) {
|
||||||
case 'mw_user_count':
|
case 'mw_user_count':
|
||||||
metrics?.user.inc(value.labels, value.value);
|
metrics?.user.inc(value.labels, value.value);
|
||||||
|
|
@ -196,8 +192,10 @@ export async function setupMetrics() {
|
||||||
break;
|
break;
|
||||||
case 'http_request_duration_seconds':
|
case 'http_request_duration_seconds':
|
||||||
// For histograms, special handling for sum and count
|
// For histograms, special handling for sum and count
|
||||||
if (value.metricName === 'http_request_duration_seconds_sum' ||
|
if (
|
||||||
value.metricName === 'http_request_duration_seconds_count') {
|
value.metricName === 'http_request_duration_seconds_sum' ||
|
||||||
|
value.metricName === 'http_request_duration_seconds_count'
|
||||||
|
) {
|
||||||
metrics?.httpRequestDuration.observe(value.labels, value.value);
|
metrics?.httpRequestDuration.observe(value.labels, value.value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -226,7 +224,7 @@ export async function setupMetrics() {
|
||||||
async function updateMetrics() {
|
async function updateMetrics() {
|
||||||
try {
|
try {
|
||||||
log.info('Fetching users from database...', { evt: 'update_metrics_start' });
|
log.info('Fetching users from database...', { evt: 'update_metrics_start' });
|
||||||
|
|
||||||
const users = await prisma.users.groupBy({
|
const users = await prisma.users.groupBy({
|
||||||
by: ['namespace'],
|
by: ['namespace'],
|
||||||
_count: true,
|
_count: true,
|
||||||
|
|
@ -237,35 +235,40 @@ async function updateMetrics() {
|
||||||
metrics?.user.reset();
|
metrics?.user.reset();
|
||||||
log.info('Reset user metrics counter', { evt: 'metrics_reset' });
|
log.info('Reset user metrics counter', { evt: 'metrics_reset' });
|
||||||
|
|
||||||
users.forEach((v) => {
|
users.forEach(v => {
|
||||||
log.info('Incrementing user metric', {
|
log.info('Incrementing user metric', {
|
||||||
evt: 'increment_metric',
|
evt: 'increment_metric',
|
||||||
namespace: v.namespace,
|
namespace: v.namespace,
|
||||||
count: v._count
|
count: v._count,
|
||||||
});
|
});
|
||||||
metrics?.user.inc({ namespace: v.namespace }, v._count);
|
metrics?.user.inc({ namespace: v.namespace }, v._count);
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info('Successfully updated metrics', { evt: 'update_metrics_complete' });
|
log.info('Successfully updated metrics', { evt: 'update_metrics_complete' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Failed to update metrics', {
|
log.error('Failed to update metrics', {
|
||||||
evt: 'update_metrics_error',
|
evt: 'update_metrics_error',
|
||||||
error: error instanceof Error ? error.message : String(error)
|
error: error instanceof Error ? error.message : String(error),
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export function to record HTTP request duration
|
// 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;
|
if (!metrics) return;
|
||||||
|
|
||||||
const labels = {
|
const labels = {
|
||||||
method,
|
method,
|
||||||
route,
|
route,
|
||||||
status_code: statusCode.toString()
|
status_code: statusCode.toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Record in both histogram and summary
|
// Record in both histogram and summary
|
||||||
metrics.httpRequestDuration.observe(labels, duration);
|
metrics.httpRequestDuration.observe(labels, duration);
|
||||||
metrics.httpRequestSummary.observe(labels, duration);
|
metrics.httpRequestSummary.observe(labels, duration);
|
||||||
|
|
@ -274,12 +277,12 @@ export function recordHttpRequest(method: string, route: string, statusCode: num
|
||||||
// Functions to match previous backend API
|
// Functions to match previous backend API
|
||||||
export function recordProviderMetrics(items: any[], hostname: string, tool?: string) {
|
export function recordProviderMetrics(items: any[], hostname: string, tool?: string) {
|
||||||
if (!metrics) return;
|
if (!metrics) return;
|
||||||
|
|
||||||
// Record hostname once per request
|
// Record hostname once per request
|
||||||
metrics.providerHostnames.inc({ hostname });
|
metrics.providerHostnames.inc({ hostname });
|
||||||
|
|
||||||
// Record status and watch metrics for each item
|
// Record status and watch metrics for each item
|
||||||
items.forEach((item) => {
|
items.forEach(item => {
|
||||||
// Record provider status
|
// Record provider status
|
||||||
metrics.providerStatuses.inc({
|
metrics.providerStatuses.inc({
|
||||||
provider_id: item.embedId ?? item.providerId,
|
provider_id: item.embedId ?? item.providerId,
|
||||||
|
|
@ -303,4 +306,4 @@ export function recordProviderMetrics(items: any[], hostname: string, tool?: str
|
||||||
|
|
||||||
export function recordCaptchaMetrics(success: boolean) {
|
export function recordCaptchaMetrics(success: boolean) {
|
||||||
metrics?.captchaSolves.inc({ success: success.toString() });
|
metrics?.captchaSolves.inc({ success: success.toString() });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { PrismaClient } from '@prisma/client'
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
const globalForPrisma = globalThis as unknown as {
|
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;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import Trakt from "trakt.tv";
|
import Trakt from 'trakt.tv';
|
||||||
const traktKeys = useRuntimeConfig().trakt;
|
const traktKeys = useRuntimeConfig().trakt;
|
||||||
|
|
||||||
if (!traktKeys) {
|
if (!traktKeys) {
|
||||||
throw new Error("Missing TraktKeys info ERROR: " + JSON.stringify(traktKeys));
|
throw new Error('Missing TraktKeys info ERROR: ' + JSON.stringify(traktKeys));
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue