Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ TMDB_API_KEY=''
TRAKT_CLIENT_ID=''
TRAKT_SECRET_ID=''

# Optional: PostgreSQL connection pool size (default: 30)
# Is an integar, only set this for optimisation other wise leave blank
DB_POOL_MAX=

# Optional: Captcha
CAPTCHA=false
CAPTCHA_CLIENT_KEY=''
3,131 changes: 2,246 additions & 885 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,23 @@
"eslint-plugin-prettier": "^5.4.0",
"nitropack": "latest",
"prettier": "^3.5.3",
"prisma": "^7.0.1"
"prisma": "^7.0.1",
"rollup": "^4.59.0"
},
"dependencies": {
"@prisma/adapter-pg": "^7.0.1",
"@prisma/client": "^7.0.1",
"@types/pg": "^8.18.0",
"cheerio": "^1.0.0",
"dotenv": "^16.4.7",
"jsonwebtoken": "^9.0.2",
"p-limit": "^7.3.0",
"pg": "^8.19.0",
"prom-client": "^15.1.3",
"tmdb-ts": "^2.0.1",
"trakt.tv": "^8.2.0",
"tweetnacl": "^1.0.3",
"uuidv7": "^1.1.0",
"whatwg-url": "^14.2.0",
"zod": "^3.24.2"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
Warnings:

- A unique constraint covering the columns `[user_id,name]` on the table `lists` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[user,device]` on the table `sessions` will be added. If there are existing duplicate values, this will fail.

*/
-- CreateIndex
CREATE INDEX "bookmarks_user_id_idx" ON "bookmarks" USING HASH ("user_id");

-- CreateIndex
CREATE UNIQUE INDEX "lists_user_id_name_unique" ON "lists"("user_id", "name");

-- CreateIndex
CREATE INDEX "progress_items_user_id_idx" ON "progress_items" USING HASH ("user_id");

-- CreateIndex
CREATE INDEX "sessions_user_idx" ON "sessions" USING HASH ("user");

-- CreateIndex
CREATE UNIQUE INDEX "sessions_user_device_unique" ON "sessions"("user", "device");

-- CreateIndex
CREATE INDEX "watch_history_user_id_watched_at_idx" ON "watch_history"("user_id", "watched_at" DESC);
14 changes: 11 additions & 3 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
generator client {
provider = "prisma-client"
output = "../generated"
moduleFormat = "esm"
provider = "prisma-client"
output = "../generated"
moduleFormat = "esm"
previewFeatures = ["relationJoins"]
}

datasource db {
Expand All @@ -18,6 +19,7 @@ model bookmarks {

@@id([tmdb_id, user_id])
@@unique([tmdb_id, user_id], map: "bookmarks_tmdb_id_user_id_unique")
@@index([user_id], type: Hash)
}

model challenge_codes {
Expand Down Expand Up @@ -49,6 +51,7 @@ model lists {
public Boolean @default(false)
list_items list_items[]

@@unique([user_id, name], map: "lists_user_id_name_unique")
@@index([user_id], map: "lists_user_id_index")
}

Expand All @@ -72,6 +75,7 @@ model progress_items {
episode_number Int?

@@unique([tmdb_id, user_id, season_id, episode_id], map: "progress_items_tmdb_id_user_id_season_id_episode_id_unique")
@@index([user_id], type: Hash)
}

model sessions {
Expand All @@ -82,6 +86,9 @@ model sessions {
expires_at DateTime @db.Timestamptz(0)
device String
user_agent String

@@unique([user, device], map: "sessions_user_device_unique")
@@index([user], type: Hash)
}

model user_group_order {
Expand Down Expand Up @@ -158,4 +165,5 @@ model watch_history {
updated_at DateTime @default(now()) @db.Timestamptz(0)

@@unique([tmdb_id, user_id, season_id, episode_id], map: "watch_history_tmdb_id_user_id_season_id_episode_id_unique")
@@index([user_id, watched_at(sort: Desc)])
}
1 change: 1 addition & 0 deletions server/routes/auth/login/start/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default defineEventHandler(async event => {

const user = await prisma.users.findUnique({
where: { public_key: body.publicKey },
select: { id: true },
});

if (!user) {
Expand Down
4 changes: 2 additions & 2 deletions server/routes/auth/register/complete.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { z } from 'zod';
import { useChallenge } from '~/utils/challenge';
import { useAuth } from '~/utils/auth';
import { randomUUID } from 'crypto';
import { uuidv7 } from 'uuidv7';
import { generateRandomNickname } from '~/utils/nickname';

const completeSchema = z.object({
Expand Down Expand Up @@ -50,7 +50,7 @@ export default defineEventHandler(async event => {
});
}

const userId = randomUUID();
const userId = uuidv7();
const now = new Date();
const nickname = generateRandomNickname();

Expand Down
1 change: 1 addition & 0 deletions server/routes/lists/[id].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { prisma } from '#imports';
export default defineEventHandler(async event => {
const id = event.context.params?.id;
const listInfo = await prisma.lists.findUnique({
relationLoadStrategy: 'join',
where: {
id: id,
},
Expand Down
39 changes: 18 additions & 21 deletions server/routes/sessions/[sid]/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,25 @@ export default defineEventHandler(async event => {
const body = await readBody(event);
const validatedBody = updateSessionSchema.parse(body);

if (validatedBody.deviceName) {
await prisma.sessions.update({
where: { id: sessionId },
data: {
device: validatedBody.deviceName,
},
});
// Use update return value directly — no redundant findUnique
let updatedSession;
try {
updatedSession = validatedBody.deviceName
? await prisma.sessions.update({
where: { id: sessionId },
data: { device: validatedBody.deviceName },
})
: targetedSession;
} catch (err: any) {
if (err.code === 'P2002') {
throw createError({
statusCode: 409,
message: 'A session with this device name already exists',
});
}
throw err;
}

const updatedSession = await prisma.sessions.findUnique({
where: { id: sessionId },
});

return {
id: updatedSession.id,
user: updatedSession.user,
Expand All @@ -65,16 +71,7 @@ export default defineEventHandler(async event => {
}

if (event.method === 'DELETE') {
const sid = event.context.params?.sid;
const sessionExists = await prisma.sessions.findUnique({
where: { id: sid },
});

if (!sessionExists) {
return { success: true };
}
const session = await useAuth().getSessionAndBump(sid);

// targetedSession already validated above — no redundant findUnique or session bump needed
await prisma.sessions.delete({
where: { id: sessionId },
});
Expand Down
8 changes: 8 additions & 0 deletions server/routes/users/@me.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ export default defineEventHandler(async event => {

const user = await prisma.users.findUnique({
where: { id: session.user },
select: {
id: true,
public_key: true,
namespace: true,
nickname: true,
profile: true,
permissions: true,
},
});

if (!user) {
Expand Down
42 changes: 24 additions & 18 deletions server/routes/users/[id]/bookmarks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useAuth } from '~/utils/auth';
import { z } from 'zod';
import { bookmarks } from '@prisma/client';

const bookmarkMetaSchema = z.object({
title: z.string(),
Expand Down Expand Up @@ -32,9 +31,16 @@ export default defineEventHandler(async event => {
if (method === 'GET') {
const bookmarks = await prisma.bookmarks.findMany({
where: { user_id: userId },
select: {
tmdb_id: true,
meta: true,
group: true,
favorite_episodes: true,
updated_at: true,
},
});

return bookmarks.map((bookmark: bookmarks) => ({
return bookmarks.map((bookmark: any) => ({
tmdbId: bookmark.tmdb_id,
meta: bookmark.meta,
group: bookmark.group,
Expand All @@ -45,21 +51,19 @@ export default defineEventHandler(async event => {

if (method === 'PUT') {
const body = await readBody(event);
const validatedBody = z.array(bookmarkDataSchema).parse(body);
const validatedBody = z.array(bookmarkDataSchema).max(1000).parse(body);

const now = new Date();
const results = [];

for (const item of validatedBody) {
const upserts = validatedBody.map((item: any) => {
// Normalize group to always be an array
const normalizedGroup = item.group
const normalizedGroup = item.group
? (Array.isArray(item.group) ? item.group : [item.group])
: [];

// Normalize favoriteEpisodes to always be an array
const normalizedFavoriteEpisodes = item.favoriteEpisodes || [];

const bookmark = await prisma.bookmarks.upsert({
return prisma.bookmarks.upsert({
where: {
tmdb_id_user_id: {
tmdb_id: item.tmdbId,
Expand All @@ -80,18 +84,20 @@ export default defineEventHandler(async event => {
favorite_episodes: normalizedFavoriteEpisodes,
updated_at: now,
} as any,
}) as bookmarks;

results.push({
tmdbId: bookmark.tmdb_id,
meta: bookmark.meta,
group: bookmark.group,
favoriteEpisodes: bookmark.favorite_episodes,
updatedAt: bookmark.updated_at,
});
}
});

if (upserts.length === 0) return [];

return results;
const bookmarks = await prisma.$transaction(upserts);

return bookmarks.map((bookmark: any) => ({
tmdbId: bookmark.tmdb_id,
meta: bookmark.meta,
group: bookmark.group,
favoriteEpisodes: bookmark.favorite_episodes,
updatedAt: bookmark.updated_at,
}));
}


Expand Down
4 changes: 2 additions & 2 deletions server/routes/users/[id]/group-order.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { randomUUID } from 'crypto';
import { uuidv7 } from 'uuidv7';
import { useAuth } from '~/utils/auth';
import { z } from 'zod';

Expand Down Expand Up @@ -38,7 +38,7 @@ export default defineEventHandler(async event => {
updated_at: new Date(),
},
create: {
id: randomUUID(),
id: uuidv7(),
user_id: userId,
group_order: validatedGroupOrder,
},
Expand Down
26 changes: 25 additions & 1 deletion server/routes/users/[id]/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,35 @@ export default defineEventHandler(async event => {
where: { user_id: userId },
});

await tx.watch_history.deleteMany({
where: { user_id: userId },
});

const userLists = await tx.lists.findMany({
where: { user_id: userId },
select: { id: true }
});
const listIds = userLists.map((l: any) => l.id);

if (listIds.length > 0) {
await tx.list_items.deleteMany({
where: { list_id: { in: listIds } },
});
}

await tx.lists.deleteMany({
where: { user_id: userId },
});

await tx.user_group_order.deleteMany({
where: { user_id: userId },
});

await tx.user_settings
.delete({
where: { id: userId },
})
.catch(() => {});
.catch(() => { });

await tx.sessions.deleteMany({
where: { user: userId },
Expand Down
Loading
Loading