diff --git a/api/src/auth.ts b/api/src/auth.ts index c9d64614..812f754f 100644 --- a/api/src/auth.ts +++ b/api/src/auth.ts @@ -23,9 +23,12 @@ const validator = TypeCompiler.Compile(Jwt); export const auth = new Elysia({ name: "auth" }) .guard({ - headers: t.Object({ - authorization: t.TemplateLiteral("Bearer ${string}"), - }), + headers: t.Object( + { + authorization: t.TemplateLiteral("Bearer ${string}"), + }, + { additionalProperties: true }, + ), }) .resolve(async ({ headers: { authorization }, error }) => { const bearer = authorization?.slice(7); diff --git a/api/src/controllers/shows/collections.ts b/api/src/controllers/shows/collections.ts index f2fd177a..3d5bb02b 100644 --- a/api/src/controllers/shows/collections.ts +++ b/api/src/controllers/shows/collections.ts @@ -1,5 +1,6 @@ import { and, eq, sql } from "drizzle-orm"; import { Elysia, t } from "elysia"; +import { auth } from "~/auth"; import { prefix } from "~/base"; import { db } from "~/db"; import { shows } from "~/db/schema"; @@ -32,12 +33,14 @@ export const collections = new Elysia({ collection: Collection, "collection-translation": CollectionTranslation, }) + .use(auth) .get( "/:id", async ({ params: { id }, headers: { "accept-language": languages }, query: { preferOriginal, with: relations }, + jwt: { sub }, error, set, }) => { @@ -52,6 +55,7 @@ export const collections = new Elysia({ fallbackLanguage: langs.includes("*"), preferOriginal, relations, + userId: sub, }); if (!ret) { return error(404, { @@ -140,6 +144,7 @@ export const collections = new Elysia({ async ({ query: { limit, after, query, sort, filter, preferOriginal }, headers: { "accept-language": languages }, + jwt: { sub }, request: { url }, }) => { const langs = processLanguages(languages); @@ -151,6 +156,7 @@ export const collections = new Elysia({ filter: and(eq(shows.kind, "collection"), filter), languages: langs, preferOriginal, + userId: sub, }); return createPage(items, { url, sort, limit }); }, @@ -222,6 +228,7 @@ export const collections = new Elysia({ params: { id }, query: { limit, after, query, sort, filter, preferOriginal }, headers: { "accept-language": languages }, + jwt: { sub }, request: { url }, error, }) => { @@ -256,6 +263,7 @@ export const collections = new Elysia({ ), languages: langs, preferOriginal, + userId: sub, }); return createPage(items, { url, sort, limit }); }, @@ -277,6 +285,7 @@ export const collections = new Elysia({ params: { id }, query: { limit, after, query, sort, filter, preferOriginal }, headers: { "accept-language": languages }, + jwt: { sub }, request: { url }, error, }) => { @@ -311,6 +320,7 @@ export const collections = new Elysia({ ), languages: langs, preferOriginal, + userId: sub, }); return createPage(items, { url, sort, limit }); }, @@ -332,6 +342,7 @@ export const collections = new Elysia({ params: { id }, query: { limit, after, query, sort, filter, preferOriginal }, headers: { "accept-language": languages }, + jwt: { sub }, request: { url }, error, }) => { @@ -362,6 +373,7 @@ export const collections = new Elysia({ filter: and(eq(shows.collectionPk, collection.pk), filter), languages: langs, preferOriginal, + userId: sub, }); return createPage(items, { url, sort, limit }); }, diff --git a/api/src/controllers/shows/movies.ts b/api/src/controllers/shows/movies.ts index 2781d171..18b969dd 100644 --- a/api/src/controllers/shows/movies.ts +++ b/api/src/controllers/shows/movies.ts @@ -1,5 +1,6 @@ import { and, eq, sql } from "drizzle-orm"; import { Elysia, t } from "elysia"; +import { auth } from "~/auth"; import { prefix } from "~/base"; import { db } from "~/db"; import { shows } from "~/db/schema"; @@ -22,12 +23,14 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) movie: Movie, "movie-translation": MovieTranslation, }) + .use(auth) .get( "/:id", async ({ params: { id }, headers: { "accept-language": languages }, query: { preferOriginal, with: relations }, + jwt: { sub }, error, set, }) => { @@ -42,6 +45,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) fallbackLanguage: langs.includes("*"), preferOriginal, relations, + userId: sub, }); if (!ret) { return error(404, { @@ -131,6 +135,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) query: { limit, after, query, sort, filter, preferOriginal }, headers: { "accept-language": languages }, request: { url }, + jwt: { sub }, }) => { const langs = processLanguages(languages); const items = await getShows({ @@ -141,6 +146,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) filter: and(eq(shows.kind, "movie"), filter), languages: langs, preferOriginal, + userId: sub, }); return createPage(items, { url, sort, limit }); }, diff --git a/api/src/controllers/staff.ts b/api/src/controllers/staff.ts index b9184b59..7ce783f3 100644 --- a/api/src/controllers/staff.ts +++ b/api/src/controllers/staff.ts @@ -25,7 +25,7 @@ import { sortToSql, } from "~/models/utils"; import { desc } from "~/models/utils/descriptions"; -import type { WatchStatus } from "~/models/watchlist"; +import type { MovieWatchStatus, SerieWatchStatus } from "~/models/watchlist"; import { showFilters, showSort } from "./shows/logic"; const staffSort = Sort( @@ -219,7 +219,7 @@ export const staffH = new Elysia({ tags: ["staff"] }) const watchStatusQ = db .select({ - watchStatus: jsonbBuildObject({ + watchStatus: jsonbBuildObject({ ...getColumns(watchlist), percent: watchlist.seenCount, }).as("watchStatus"), diff --git a/api/src/controllers/studios.ts b/api/src/controllers/studios.ts index ba8a5290..d9682b78 100644 --- a/api/src/controllers/studios.ts +++ b/api/src/controllers/studios.ts @@ -1,5 +1,6 @@ import { type SQL, and, eq, exists, sql } from "drizzle-orm"; import Elysia, { t } from "elysia"; +import { auth } from "~/auth"; import { prefix } from "~/base"; import { db } from "~/db"; import { @@ -127,6 +128,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] }) studio: Studio, "studio-translation": StudioTranslation, }) + .use(auth) .get( "/:id", async ({ @@ -301,6 +303,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] }) params: { id }, query: { limit, after, query, sort, filter, preferOriginal }, headers: { "accept-language": languages }, + jwt: { sub }, request: { url }, error, }) => { @@ -339,6 +342,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] }) ), languages: langs, preferOriginal, + userId: sub, }); return createPage(items, { url, sort, limit }); }, @@ -360,6 +364,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] }) params: { id }, query: { limit, after, query, sort, filter, preferOriginal }, headers: { "accept-language": languages }, + jwt: { sub }, request: { url }, error, }) => { @@ -399,6 +404,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] }) ), languages: langs, preferOriginal, + userId: sub, }); return createPage(items, { url, sort, limit }); }, @@ -420,6 +426,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] }) params: { id }, query: { limit, after, query, sort, filter, preferOriginal }, headers: { "accept-language": languages }, + jwt: { sub }, request: { url }, error, }) => { @@ -459,6 +466,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] }) ), languages: langs, preferOriginal, + userId: sub, }); return createPage(items, { url, sort, limit }); }, diff --git a/api/src/controllers/watchlist.ts b/api/src/controllers/watchlist.ts index cc6acfac..ae3256e4 100644 --- a/api/src/controllers/watchlist.ts +++ b/api/src/controllers/watchlist.ts @@ -22,11 +22,11 @@ import { MovieWatchStatus, SerieWatchStatus } from "~/models/watchlist"; import { getShows, showFilters, showSort, watchStatusQ } from "./shows/logic"; async function setWatchStatus({ - showFilter, + show, status, userId, }: { - showFilter: { id: SQL; kind: "movie" | "serie" }; + show: { pk: number; kind: "movie" | "serie" }; status: SerieWatchStatus; userId: string; }) { @@ -48,17 +48,12 @@ async function setWatchStatus({ .returning({ pk: profiles.pk }); } - const showQ = db - .select({ pk: shows.pk }) - .from(shows) - .where(and(showFilter.id, eq(shows.kind, showFilter.kind))); - const [ret] = await db .insert(watchlist) .values({ ...status, profilePk: profile.pk, - showPk: sql`${showQ}`, + showPk: show.pk, }) .onConflictDoUpdate({ target: [watchlist.profilePk, watchlist.showPk], @@ -70,7 +65,7 @@ async function setWatchStatus({ "seenCount", ]), // do not reset movie's progress during drop - ...(showFilter.kind === "movie" && status.status !== "dropped" + ...(show.kind === "movie" && status.status !== "dropped" ? { seenCount: sql`excluded.seen_count` } : {}), }, @@ -205,12 +200,25 @@ export const watchlistH = new Elysia({ tags: ["profiles"] }) ) .post( "/series/:id/watchstatus", - async ({ params: { id }, body, jwt: { sub } }) => { + async ({ params: { id }, body, jwt: { sub }, error }) => { + const [show] = await db + .select({ pk: shows.pk }) + .from(shows) + .where( + and( + eq(shows.kind, "serie"), + isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id), + ), + ); + + if (!show) { + return error(404, { + status: 404, + message: `No serie found for the id/slug: '${id}'.`, + }); + } return await setWatchStatus({ - showFilter: { - kind: "serie", - id: isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id), - }, + show: { pk: show.pk, kind: "serie" }, userId: sub, status: body, }); @@ -226,18 +234,33 @@ export const watchlistH = new Elysia({ tags: ["profiles"] }) body: SerieWatchStatus, response: { 200: t.Union([SerieWatchStatus, DbMetadata]), + 404: KError, }, permissions: ["core.read"], }, ) .post( "/movies/:id/watchstatus", - async ({ params: { id }, body, jwt: { sub } }) => { + async ({ params: { id }, body, jwt: { sub }, error }) => { + const [show] = await db + .select({ pk: shows.pk }) + .from(shows) + .where( + and( + eq(shows.kind, "movie"), + isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id), + ), + ); + + if (!show) { + return error(404, { + status: 404, + message: `No movie found for the id/slug: '${id}'.`, + }); + } + return await setWatchStatus({ - showFilter: { - kind: "movie", - id: isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id), - }, + show: { pk: show.pk, kind: "movie" }, userId: sub, status: { ...body, @@ -258,6 +281,7 @@ export const watchlistH = new Elysia({ tags: ["profiles"] }) body: t.Omit(MovieWatchStatus, ["percent"]), response: { 200: t.Union([MovieWatchStatus, DbMetadata]), + 404: KError, }, permissions: ["core.read"], }, diff --git a/api/src/models/serie.ts b/api/src/models/serie.ts index 2757f493..3e4c3f2a 100644 --- a/api/src/models/serie.ts +++ b/api/src/models/serie.ts @@ -84,7 +84,7 @@ export const FullSerie = t.Intersect([ firstEntry: t.Optional(Entry), }), ]); -export type FullMovie = Prettify; +export type FullSerie = Prettify; export const SeedSerie = t.Intersect([ t.Omit(BaseSerie, ["kind", "nextRefresh"]), diff --git a/api/tests/helpers/series-helper.ts b/api/tests/helpers/series-helper.ts index d585ef7a..ac856226 100644 --- a/api/tests/helpers/series-helper.ts +++ b/api/tests/helpers/series-helper.ts @@ -41,6 +41,25 @@ export const getSerie = async ( return [resp, body] as const; }; +export const getSeries = async ({ + langs, + ...query +}: { langs?: string; preferOriginal?: boolean; with?: string[] }) => { + const resp = await app.handle( + new Request(buildUrl("series", query), { + method: "GET", + headers: langs + ? { + "Accept-Language": langs, + ...(await getJwtHeaders()), + } + : await getJwtHeaders(), + }), + ); + const body = await resp.json(); + return [resp, body] as const; +}; + export const getSeasons = async ( serie: string, { @@ -166,7 +185,7 @@ export const getNews = async ({ export const setSerieStatus = async (id: string, status: SerieWatchStatus) => { const resp = await app.handle( - new Request(buildUrl(`movies/${id}/watchstatus`), { + new Request(buildUrl(`series/${id}/watchstatus`), { method: "POST", body: JSON.stringify(status), headers: { diff --git a/api/tests/manual.ts b/api/tests/manual.ts index 4d4cb906..6ad8c521 100644 --- a/api/tests/manual.ts +++ b/api/tests/manual.ts @@ -1,24 +1,29 @@ -import { processImages } from "~/controllers/seed/images"; import { db, migrate } from "~/db"; -import { mqueue, shows, videos } from "~/db/schema"; -import { madeInAbyss, madeInAbyssVideo } from "~/models/examples"; -import { createSerie, createVideo, getSerie } from "./helpers"; +import { profiles, shows } from "~/db/schema"; +import { madeInAbyss } from "~/models/examples"; +import { createSerie, getSerie, setSerieStatus } from "./helpers"; +import { getJwtHeaders } from "./helpers/jwt"; // test file used to run manually using `bun tests/manual.ts` await migrate(); await db.delete(shows); -await db.delete(videos); -await db.delete(mqueue); +await db.delete(profiles); -const [_, vid] = await createVideo(madeInAbyssVideo); -console.log(vid); -const [__, ser] = await createSerie(madeInAbyss); +console.log(await getJwtHeaders()); + +const [_, ser] = await createSerie(madeInAbyss); console.log(ser); +const [__, ret] = await setSerieStatus(madeInAbyss.slug, { + status: "watching", + startedAt: "2024-12-21", + completedAt: "2024-12-21", + seenCount: 2, + score: 85, +}); +console.log(ret); -await processImages(); - -const [___, got] = await getSerie(madeInAbyss.slug, { with: ["translations"] }); -console.log(got); +const [___, got] = await getSerie(madeInAbyss.slug, {}); +console.log(JSON.stringify(got, undefined, 4)); process.exit(0); diff --git a/api/tests/misc/images.test.ts b/api/tests/misc/images.test.ts index c59a3a92..86d6252a 100644 --- a/api/tests/misc/images.test.ts +++ b/api/tests/misc/images.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, it } from "bun:test"; +import { describe, expect, it } from "bun:test"; import { eq } from "drizzle-orm"; import { defaultBlurhash, processImages } from "~/controllers/seed/images"; import { db } from "~/db"; @@ -6,21 +6,19 @@ import { mqueue, shows, staff, studios, videos } from "~/db/schema"; import { madeInAbyss } from "~/models/examples"; import { createSerie } from "../helpers"; -beforeAll(async () => { - await db.delete(shows); - await db.delete(studios); - await db.delete(staff); - await db.delete(videos); - await db.delete(mqueue); - - await createSerie(madeInAbyss); - const release = await processImages(); - // remove notifications to prevent other images to be downloaded (do not curl 20000 images for nothing) - release(); -}); - describe("images", () => { it("Create a serie download images", async () => { + await db.delete(shows); + await db.delete(studios); + await db.delete(staff); + await db.delete(videos); + await db.delete(mqueue); + + await createSerie(madeInAbyss); + const release = await processImages(); + // remove notifications to prevent other images to be downloaded (do not curl 20000 images for nothing) + release(); + const ret = await db.query.shows.findFirst({ where: eq(shows.slug, madeInAbyss.slug), }); diff --git a/api/tests/movies/watchstatus.test.ts b/api/tests/movies/watchstatus.test.ts index 96025ab3..609133d8 100644 --- a/api/tests/movies/watchstatus.test.ts +++ b/api/tests/movies/watchstatus.test.ts @@ -81,6 +81,14 @@ describe("Set & get watch status", () => { }); it("Return watchstatus in /movies/:id", async () => { + const [r, b] = await setMovieStatus(bubble.slug, { + status: "rewatching", + // we still need to specify all values + completedAt: "2024-12-21", + score: 85, + }); + expectStatus(r, b).toBe(200); + const [resp, body] = await getMovie(bubble.slug, {}); expectStatus(resp, body).toBe(200); expect(body.slug).toBe(bubble.slug);