mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-03 05:34:23 -04:00
Add watchStatus apis (#874)
This commit is contained in:
commit
572e763a61
@ -97,6 +97,6 @@ RABBITMQ_DEFAULT_PASS=aohohunuhouhuhhoahothonseuhaoensuthoaentsuhha
|
|||||||
|
|
||||||
# v5 stuff, does absolutely nothing on master (aka: you can delete this)
|
# v5 stuff, does absolutely nothing on master (aka: you can delete this)
|
||||||
EXTRA_CLAIMS='{"permissions": ["core.read"], "verified": false}'
|
EXTRA_CLAIMS='{"permissions": ["core.read"], "verified": false}'
|
||||||
FIRST_USER_CLAIMS='{"permissions": ["user.read", "users.write", "users.delete", "core.read"], "verified": true}'
|
FIRST_USER_CLAIMS='{"permissions": ["users.read", "users.write", "users.delete", "core.read"], "verified": true}'
|
||||||
GUEST_CLAIMS='{"permissions": ["core.read"]}'
|
GUEST_CLAIMS='{"permissions": ["core.read"]}'
|
||||||
PROTECTED_CLAIMS="permissions,verified"
|
PROTECTED_CLAIMS="permissions,verified"
|
||||||
|
1
.github/workflows/auth-hurl.yml
vendored
1
.github/workflows/auth-hurl.yml
vendored
@ -52,6 +52,7 @@ jobs:
|
|||||||
hurl --error-format long --variable host=http://localhost:4568 tests/*
|
hurl --error-format long --variable host=http://localhost:4568 tests/*
|
||||||
env:
|
env:
|
||||||
POSTGRES_SERVER: localhost
|
POSTGRES_SERVER: localhost
|
||||||
|
FIRST_USER_CLAIMS: '{"permissions": ["users.read"]}'
|
||||||
|
|
||||||
- name: Show logs
|
- name: Show logs
|
||||||
working-directory: ./auth
|
working-directory: ./auth
|
||||||
|
@ -10,7 +10,7 @@ JWT_SECRET=
|
|||||||
# used to verify who's making the jwt
|
# used to verify who's making the jwt
|
||||||
JWT_ISSUER=$PUBLIC_URL
|
JWT_ISSUER=$PUBLIC_URL
|
||||||
# keibi's server to retrieve the public jwt secret
|
# keibi's server to retrieve the public jwt secret
|
||||||
AUHT_SERVER=http://auth:4568
|
AUTH_SERVER=http://auth:4568
|
||||||
|
|
||||||
IMAGES_PATH=./images
|
IMAGES_PATH=./images
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { TypeCompiler } from "@sinclair/typebox/compiler";
|
import { TypeCompiler } from "@sinclair/typebox/compiler";
|
||||||
import Elysia, { t } from "elysia";
|
import Elysia, { t } from "elysia";
|
||||||
import { createRemoteJWKSet, jwtVerify } from "jose";
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
||||||
|
import { KError } from "./models/error";
|
||||||
|
|
||||||
const jwtSecret = process.env.JWT_SECRET
|
const jwtSecret = process.env.JWT_SECRET
|
||||||
? new TextEncoder().encode(process.env.JWT_SECRET)
|
? new TextEncoder().encode(process.env.JWT_SECRET)
|
||||||
@ -22,9 +23,12 @@ const validator = TypeCompiler.Compile(Jwt);
|
|||||||
|
|
||||||
export const auth = new Elysia({ name: "auth" })
|
export const auth = new Elysia({ name: "auth" })
|
||||||
.guard({
|
.guard({
|
||||||
headers: t.Object({
|
headers: t.Object(
|
||||||
authorization: t.TemplateLiteral("Bearer ${string}"),
|
{
|
||||||
}),
|
authorization: t.TemplateLiteral("Bearer ${string}"),
|
||||||
|
},
|
||||||
|
{ additionalProperties: true },
|
||||||
|
),
|
||||||
})
|
})
|
||||||
.resolve(async ({ headers: { authorization }, error }) => {
|
.resolve(async ({ headers: { authorization }, error }) => {
|
||||||
const bearer = authorization?.slice(7);
|
const bearer = authorization?.slice(7);
|
||||||
@ -69,3 +73,33 @@ export const auth = new Elysia({ name: "auth" })
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.as("plugin");
|
.as("plugin");
|
||||||
|
|
||||||
|
const User = t.Object({
|
||||||
|
id: t.String({ format: "uuid" }),
|
||||||
|
username: t.String(),
|
||||||
|
email: t.String({ format: "email" }),
|
||||||
|
createdDate: t.String({ format: "date-time" }),
|
||||||
|
lastSeen: t.String({ format: "date-time" }),
|
||||||
|
claims: t.Record(t.String(), t.Any()),
|
||||||
|
oidc: t.Record(
|
||||||
|
t.String(),
|
||||||
|
t.Object({
|
||||||
|
id: t.String({ format: "uuid" }),
|
||||||
|
username: t.String(),
|
||||||
|
profileUrl: t.Nullable(t.String({ format: "url" })),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
const UserC = TypeCompiler.Compile(t.Union([User, KError]));
|
||||||
|
|
||||||
|
export async function getUserInfo(
|
||||||
|
id: string,
|
||||||
|
headers: { authorization: string },
|
||||||
|
) {
|
||||||
|
const resp = await fetch(
|
||||||
|
new URL(`/auth/users/${id}`, process.env.AUTH_SERVER ?? "http://auth:4568"),
|
||||||
|
{ headers },
|
||||||
|
);
|
||||||
|
|
||||||
|
return UserC.Decode(await resp.json());
|
||||||
|
}
|
||||||
|
@ -11,6 +11,7 @@ import { showsH } from "./controllers/shows/shows";
|
|||||||
import { staffH } from "./controllers/staff";
|
import { staffH } from "./controllers/staff";
|
||||||
import { studiosH } from "./controllers/studios";
|
import { studiosH } from "./controllers/studios";
|
||||||
import { videosH } from "./controllers/videos";
|
import { videosH } from "./controllers/videos";
|
||||||
|
import { watchlistH } from "./controllers/watchlist";
|
||||||
import type { KError } from "./models/error";
|
import type { KError } from "./models/error";
|
||||||
|
|
||||||
export const base = new Elysia({ name: "base" })
|
export const base = new Elysia({ name: "base" })
|
||||||
@ -93,4 +94,5 @@ export const app = new Elysia({ prefix })
|
|||||||
permissions: ["core.write"],
|
permissions: ["core.write"],
|
||||||
},
|
},
|
||||||
(app) => app.use(videosH).use(seed),
|
(app) => app.use(videosH).use(seed),
|
||||||
);
|
)
|
||||||
|
.use(watchlistH);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { and, eq, sql } from "drizzle-orm";
|
import { and, eq, sql } from "drizzle-orm";
|
||||||
import { Elysia, t } from "elysia";
|
import { Elysia, t } from "elysia";
|
||||||
|
import { auth } from "~/auth";
|
||||||
import { prefix } from "~/base";
|
import { prefix } from "~/base";
|
||||||
import { db } from "~/db";
|
import { db } from "~/db";
|
||||||
import { shows } from "~/db/schema";
|
import { shows } from "~/db/schema";
|
||||||
@ -32,12 +33,14 @@ export const collections = new Elysia({
|
|||||||
collection: Collection,
|
collection: Collection,
|
||||||
"collection-translation": CollectionTranslation,
|
"collection-translation": CollectionTranslation,
|
||||||
})
|
})
|
||||||
|
.use(auth)
|
||||||
.get(
|
.get(
|
||||||
"/:id",
|
"/:id",
|
||||||
async ({
|
async ({
|
||||||
params: { id },
|
params: { id },
|
||||||
headers: { "accept-language": languages },
|
headers: { "accept-language": languages },
|
||||||
query: { preferOriginal, with: relations },
|
query: { preferOriginal, with: relations },
|
||||||
|
jwt: { sub },
|
||||||
error,
|
error,
|
||||||
set,
|
set,
|
||||||
}) => {
|
}) => {
|
||||||
@ -52,6 +55,7 @@ export const collections = new Elysia({
|
|||||||
fallbackLanguage: langs.includes("*"),
|
fallbackLanguage: langs.includes("*"),
|
||||||
preferOriginal,
|
preferOriginal,
|
||||||
relations,
|
relations,
|
||||||
|
userId: sub,
|
||||||
});
|
});
|
||||||
if (!ret) {
|
if (!ret) {
|
||||||
return error(404, {
|
return error(404, {
|
||||||
@ -140,6 +144,7 @@ export const collections = new Elysia({
|
|||||||
async ({
|
async ({
|
||||||
query: { limit, after, query, sort, filter, preferOriginal },
|
query: { limit, after, query, sort, filter, preferOriginal },
|
||||||
headers: { "accept-language": languages },
|
headers: { "accept-language": languages },
|
||||||
|
jwt: { sub },
|
||||||
request: { url },
|
request: { url },
|
||||||
}) => {
|
}) => {
|
||||||
const langs = processLanguages(languages);
|
const langs = processLanguages(languages);
|
||||||
@ -151,6 +156,7 @@ export const collections = new Elysia({
|
|||||||
filter: and(eq(shows.kind, "collection"), filter),
|
filter: and(eq(shows.kind, "collection"), filter),
|
||||||
languages: langs,
|
languages: langs,
|
||||||
preferOriginal,
|
preferOriginal,
|
||||||
|
userId: sub,
|
||||||
});
|
});
|
||||||
return createPage(items, { url, sort, limit });
|
return createPage(items, { url, sort, limit });
|
||||||
},
|
},
|
||||||
@ -222,6 +228,7 @@ export const collections = new Elysia({
|
|||||||
params: { id },
|
params: { id },
|
||||||
query: { limit, after, query, sort, filter, preferOriginal },
|
query: { limit, after, query, sort, filter, preferOriginal },
|
||||||
headers: { "accept-language": languages },
|
headers: { "accept-language": languages },
|
||||||
|
jwt: { sub },
|
||||||
request: { url },
|
request: { url },
|
||||||
error,
|
error,
|
||||||
}) => {
|
}) => {
|
||||||
@ -256,6 +263,7 @@ export const collections = new Elysia({
|
|||||||
),
|
),
|
||||||
languages: langs,
|
languages: langs,
|
||||||
preferOriginal,
|
preferOriginal,
|
||||||
|
userId: sub,
|
||||||
});
|
});
|
||||||
return createPage(items, { url, sort, limit });
|
return createPage(items, { url, sort, limit });
|
||||||
},
|
},
|
||||||
@ -277,6 +285,7 @@ export const collections = new Elysia({
|
|||||||
params: { id },
|
params: { id },
|
||||||
query: { limit, after, query, sort, filter, preferOriginal },
|
query: { limit, after, query, sort, filter, preferOriginal },
|
||||||
headers: { "accept-language": languages },
|
headers: { "accept-language": languages },
|
||||||
|
jwt: { sub },
|
||||||
request: { url },
|
request: { url },
|
||||||
error,
|
error,
|
||||||
}) => {
|
}) => {
|
||||||
@ -311,6 +320,7 @@ export const collections = new Elysia({
|
|||||||
),
|
),
|
||||||
languages: langs,
|
languages: langs,
|
||||||
preferOriginal,
|
preferOriginal,
|
||||||
|
userId: sub,
|
||||||
});
|
});
|
||||||
return createPage(items, { url, sort, limit });
|
return createPage(items, { url, sort, limit });
|
||||||
},
|
},
|
||||||
@ -332,6 +342,7 @@ export const collections = new Elysia({
|
|||||||
params: { id },
|
params: { id },
|
||||||
query: { limit, after, query, sort, filter, preferOriginal },
|
query: { limit, after, query, sort, filter, preferOriginal },
|
||||||
headers: { "accept-language": languages },
|
headers: { "accept-language": languages },
|
||||||
|
jwt: { sub },
|
||||||
request: { url },
|
request: { url },
|
||||||
error,
|
error,
|
||||||
}) => {
|
}) => {
|
||||||
@ -362,6 +373,7 @@ export const collections = new Elysia({
|
|||||||
filter: and(eq(shows.collectionPk, collection.pk), filter),
|
filter: and(eq(shows.collectionPk, collection.pk), filter),
|
||||||
languages: langs,
|
languages: langs,
|
||||||
preferOriginal,
|
preferOriginal,
|
||||||
|
userId: sub,
|
||||||
});
|
});
|
||||||
return createPage(items, { url, sort, limit });
|
return createPage(items, { url, sort, limit });
|
||||||
},
|
},
|
||||||
|
@ -1,20 +1,9 @@
|
|||||||
import {
|
import { type SQL, and, eq, exists, ne, sql } from "drizzle-orm";
|
||||||
type SQL,
|
|
||||||
type Subquery,
|
|
||||||
and,
|
|
||||||
desc,
|
|
||||||
eq,
|
|
||||||
exists,
|
|
||||||
ne,
|
|
||||||
sql,
|
|
||||||
} from "drizzle-orm";
|
|
||||||
import type { PgSelect } from "drizzle-orm/pg-core";
|
|
||||||
import { db } from "~/db";
|
import { db } from "~/db";
|
||||||
import {
|
import {
|
||||||
entries,
|
entries,
|
||||||
entryTranslations,
|
entryTranslations,
|
||||||
entryVideoJoin,
|
entryVideoJoin,
|
||||||
history,
|
|
||||||
profiles,
|
profiles,
|
||||||
showStudioJoin,
|
showStudioJoin,
|
||||||
showTranslations,
|
showTranslations,
|
||||||
@ -46,8 +35,19 @@ import {
|
|||||||
sortToSql,
|
sortToSql,
|
||||||
} from "~/models/utils";
|
} from "~/models/utils";
|
||||||
import type { EmbeddedVideo } from "~/models/video";
|
import type { EmbeddedVideo } from "~/models/video";
|
||||||
|
import { WatchlistStatus } from "~/models/watchlist";
|
||||||
import { entryVideosQ, getEntryProgressQ, mapProgress } from "../entries";
|
import { entryVideosQ, getEntryProgressQ, mapProgress } from "../entries";
|
||||||
|
|
||||||
|
export const watchStatusQ = db
|
||||||
|
.select({
|
||||||
|
...getColumns(watchlist),
|
||||||
|
percent: sql`${watchlist.seenCount}`.as("percent"),
|
||||||
|
})
|
||||||
|
.from(watchlist)
|
||||||
|
.leftJoin(profiles, eq(watchlist.profilePk, profiles.pk))
|
||||||
|
.where(eq(profiles.id, sql.placeholder("userId")))
|
||||||
|
.as("watchstatus");
|
||||||
|
|
||||||
export const showFilters: FilterDef = {
|
export const showFilters: FilterDef = {
|
||||||
genres: {
|
genres: {
|
||||||
column: shows.genres,
|
column: shows.genres,
|
||||||
@ -70,6 +70,11 @@ export const showFilters: FilterDef = {
|
|||||||
type: "string",
|
type: "string",
|
||||||
isArray: true,
|
isArray: true,
|
||||||
},
|
},
|
||||||
|
watchStatus: {
|
||||||
|
column: watchStatusQ.status,
|
||||||
|
type: "enum",
|
||||||
|
values: WatchlistStatus.enum,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
export const showSort = Sort(
|
export const showSort = Sort(
|
||||||
{
|
{
|
||||||
@ -80,6 +85,7 @@ export const showSort = Sort(
|
|||||||
endAir: shows.endAir,
|
endAir: shows.endAir,
|
||||||
createdAt: shows.createdAt,
|
createdAt: shows.createdAt,
|
||||||
nextRefresh: shows.nextRefresh,
|
nextRefresh: shows.nextRefresh,
|
||||||
|
watchStatus: watchStatusQ.status,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: ["slug"],
|
default: ["slug"],
|
||||||
@ -196,11 +202,9 @@ const showRelations = {
|
|||||||
nextEntry: ({
|
nextEntry: ({
|
||||||
languages,
|
languages,
|
||||||
userId,
|
userId,
|
||||||
watchStatusQ,
|
|
||||||
}: {
|
}: {
|
||||||
languages: string[];
|
languages: string[];
|
||||||
userId: string;
|
userId: string;
|
||||||
watchStatusQ: Subquery;
|
|
||||||
}) => {
|
}) => {
|
||||||
const transQ = db
|
const transQ = db
|
||||||
.selectDistinctOn([entryTranslations.pk])
|
.selectDistinctOn([entryTranslations.pk])
|
||||||
@ -228,9 +232,7 @@ const showRelations = {
|
|||||||
.innerJoin(transQ, eq(entries.pk, transQ.pk))
|
.innerJoin(transQ, eq(entries.pk, transQ.pk))
|
||||||
.leftJoin(progressQ, eq(entries.pk, progressQ.entryPk))
|
.leftJoin(progressQ, eq(entries.pk, progressQ.entryPk))
|
||||||
.leftJoinLateral(entryVideosQ, sql`true`)
|
.leftJoinLateral(entryVideosQ, sql`true`)
|
||||||
.where(
|
.where(eq(watchStatusQ.nextEntry, entries.pk))
|
||||||
eq((watchStatusQ as unknown as typeof watchlist).nextEntry, entries.pk),
|
|
||||||
)
|
|
||||||
.as("nextEntry");
|
.as("nextEntry");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -272,16 +274,6 @@ export async function getShows({
|
|||||||
)
|
)
|
||||||
.as("t");
|
.as("t");
|
||||||
|
|
||||||
const watchStatusQ = db
|
|
||||||
.select({
|
|
||||||
...getColumns(watchlist),
|
|
||||||
percent: sql`${watchlist.seenCount}`.as("percent"),
|
|
||||||
})
|
|
||||||
.from(watchlist)
|
|
||||||
.leftJoin(profiles, eq(watchlist.profilePk, profiles.pk))
|
|
||||||
.where(eq(profiles.id, userId))
|
|
||||||
.as("watchstatus");
|
|
||||||
|
|
||||||
return await db
|
return await db
|
||||||
.select({
|
.select({
|
||||||
...getColumns(shows),
|
...getColumns(shows),
|
||||||
@ -302,11 +294,7 @@ export async function getShows({
|
|||||||
|
|
||||||
watchStatus: getColumns(watchStatusQ),
|
watchStatus: getColumns(watchStatusQ),
|
||||||
|
|
||||||
...buildRelations(relations, showRelations, {
|
...buildRelations(relations, showRelations, { languages, userId }),
|
||||||
languages,
|
|
||||||
userId,
|
|
||||||
watchStatusQ,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
.from(shows)
|
.from(shows)
|
||||||
.leftJoin(watchStatusQ, eq(shows.pk, watchStatusQ.showPk))
|
.leftJoin(watchStatusQ, eq(shows.pk, watchStatusQ.showPk))
|
||||||
@ -327,5 +315,6 @@ export async function getShows({
|
|||||||
: sortToSql(sort)),
|
: sortToSql(sort)),
|
||||||
shows.pk,
|
shows.pk,
|
||||||
)
|
)
|
||||||
.limit(limit);
|
.limit(limit)
|
||||||
|
.execute({ userId });
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { and, eq, sql } from "drizzle-orm";
|
import { and, eq, sql } from "drizzle-orm";
|
||||||
import { Elysia, t } from "elysia";
|
import { Elysia, t } from "elysia";
|
||||||
|
import { auth } from "~/auth";
|
||||||
import { prefix } from "~/base";
|
import { prefix } from "~/base";
|
||||||
import { db } from "~/db";
|
import { db } from "~/db";
|
||||||
import { shows } from "~/db/schema";
|
import { shows } from "~/db/schema";
|
||||||
@ -22,12 +23,14 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
|
|||||||
movie: Movie,
|
movie: Movie,
|
||||||
"movie-translation": MovieTranslation,
|
"movie-translation": MovieTranslation,
|
||||||
})
|
})
|
||||||
|
.use(auth)
|
||||||
.get(
|
.get(
|
||||||
"/:id",
|
"/:id",
|
||||||
async ({
|
async ({
|
||||||
params: { id },
|
params: { id },
|
||||||
headers: { "accept-language": languages },
|
headers: { "accept-language": languages },
|
||||||
query: { preferOriginal, with: relations },
|
query: { preferOriginal, with: relations },
|
||||||
|
jwt: { sub },
|
||||||
error,
|
error,
|
||||||
set,
|
set,
|
||||||
}) => {
|
}) => {
|
||||||
@ -42,6 +45,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
|
|||||||
fallbackLanguage: langs.includes("*"),
|
fallbackLanguage: langs.includes("*"),
|
||||||
preferOriginal,
|
preferOriginal,
|
||||||
relations,
|
relations,
|
||||||
|
userId: sub,
|
||||||
});
|
});
|
||||||
if (!ret) {
|
if (!ret) {
|
||||||
return error(404, {
|
return error(404, {
|
||||||
@ -131,6 +135,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
|
|||||||
query: { limit, after, query, sort, filter, preferOriginal },
|
query: { limit, after, query, sort, filter, preferOriginal },
|
||||||
headers: { "accept-language": languages },
|
headers: { "accept-language": languages },
|
||||||
request: { url },
|
request: { url },
|
||||||
|
jwt: { sub },
|
||||||
}) => {
|
}) => {
|
||||||
const langs = processLanguages(languages);
|
const langs = processLanguages(languages);
|
||||||
const items = await getShows({
|
const items = await getShows({
|
||||||
@ -141,6 +146,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
|
|||||||
filter: and(eq(shows.kind, "movie"), filter),
|
filter: and(eq(shows.kind, "movie"), filter),
|
||||||
languages: langs,
|
languages: langs,
|
||||||
preferOriginal,
|
preferOriginal,
|
||||||
|
userId: sub,
|
||||||
});
|
});
|
||||||
return createPage(items, { url, sort, limit });
|
return createPage(items, { url, sort, limit });
|
||||||
},
|
},
|
||||||
|
@ -25,7 +25,7 @@ import {
|
|||||||
sortToSql,
|
sortToSql,
|
||||||
} from "~/models/utils";
|
} from "~/models/utils";
|
||||||
import { desc } from "~/models/utils/descriptions";
|
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";
|
import { showFilters, showSort } from "./shows/logic";
|
||||||
|
|
||||||
const staffSort = Sort(
|
const staffSort = Sort(
|
||||||
@ -219,7 +219,7 @@ export const staffH = new Elysia({ tags: ["staff"] })
|
|||||||
|
|
||||||
const watchStatusQ = db
|
const watchStatusQ = db
|
||||||
.select({
|
.select({
|
||||||
watchStatus: jsonbBuildObject<WatchStatus>({
|
watchStatus: jsonbBuildObject<MovieWatchStatus & SerieWatchStatus>({
|
||||||
...getColumns(watchlist),
|
...getColumns(watchlist),
|
||||||
percent: watchlist.seenCount,
|
percent: watchlist.seenCount,
|
||||||
}).as("watchStatus"),
|
}).as("watchStatus"),
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { type SQL, and, eq, exists, sql } from "drizzle-orm";
|
import { type SQL, and, eq, exists, sql } from "drizzle-orm";
|
||||||
import Elysia, { t } from "elysia";
|
import Elysia, { t } from "elysia";
|
||||||
|
import { auth } from "~/auth";
|
||||||
import { prefix } from "~/base";
|
import { prefix } from "~/base";
|
||||||
import { db } from "~/db";
|
import { db } from "~/db";
|
||||||
import {
|
import {
|
||||||
@ -127,6 +128,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
|
|||||||
studio: Studio,
|
studio: Studio,
|
||||||
"studio-translation": StudioTranslation,
|
"studio-translation": StudioTranslation,
|
||||||
})
|
})
|
||||||
|
.use(auth)
|
||||||
.get(
|
.get(
|
||||||
"/:id",
|
"/:id",
|
||||||
async ({
|
async ({
|
||||||
@ -301,6 +303,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
|
|||||||
params: { id },
|
params: { id },
|
||||||
query: { limit, after, query, sort, filter, preferOriginal },
|
query: { limit, after, query, sort, filter, preferOriginal },
|
||||||
headers: { "accept-language": languages },
|
headers: { "accept-language": languages },
|
||||||
|
jwt: { sub },
|
||||||
request: { url },
|
request: { url },
|
||||||
error,
|
error,
|
||||||
}) => {
|
}) => {
|
||||||
@ -339,6 +342,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
|
|||||||
),
|
),
|
||||||
languages: langs,
|
languages: langs,
|
||||||
preferOriginal,
|
preferOriginal,
|
||||||
|
userId: sub,
|
||||||
});
|
});
|
||||||
return createPage(items, { url, sort, limit });
|
return createPage(items, { url, sort, limit });
|
||||||
},
|
},
|
||||||
@ -360,6 +364,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
|
|||||||
params: { id },
|
params: { id },
|
||||||
query: { limit, after, query, sort, filter, preferOriginal },
|
query: { limit, after, query, sort, filter, preferOriginal },
|
||||||
headers: { "accept-language": languages },
|
headers: { "accept-language": languages },
|
||||||
|
jwt: { sub },
|
||||||
request: { url },
|
request: { url },
|
||||||
error,
|
error,
|
||||||
}) => {
|
}) => {
|
||||||
@ -399,6 +404,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
|
|||||||
),
|
),
|
||||||
languages: langs,
|
languages: langs,
|
||||||
preferOriginal,
|
preferOriginal,
|
||||||
|
userId: sub,
|
||||||
});
|
});
|
||||||
return createPage(items, { url, sort, limit });
|
return createPage(items, { url, sort, limit });
|
||||||
},
|
},
|
||||||
@ -420,6 +426,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
|
|||||||
params: { id },
|
params: { id },
|
||||||
query: { limit, after, query, sort, filter, preferOriginal },
|
query: { limit, after, query, sort, filter, preferOriginal },
|
||||||
headers: { "accept-language": languages },
|
headers: { "accept-language": languages },
|
||||||
|
jwt: { sub },
|
||||||
request: { url },
|
request: { url },
|
||||||
error,
|
error,
|
||||||
}) => {
|
}) => {
|
||||||
@ -459,6 +466,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
|
|||||||
),
|
),
|
||||||
languages: langs,
|
languages: langs,
|
||||||
preferOriginal,
|
preferOriginal,
|
||||||
|
userId: sub,
|
||||||
});
|
});
|
||||||
return createPage(items, { url, sort, limit });
|
return createPage(items, { url, sort, limit });
|
||||||
},
|
},
|
||||||
|
288
api/src/controllers/watchlist.ts
Normal file
288
api/src/controllers/watchlist.ts
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
import { type SQL, and, eq, isNotNull, isNull, sql } from "drizzle-orm";
|
||||||
|
import Elysia, { t } from "elysia";
|
||||||
|
import { auth, getUserInfo } from "~/auth";
|
||||||
|
import { db } from "~/db";
|
||||||
|
import { profiles, shows } from "~/db/schema";
|
||||||
|
import { watchlist } from "~/db/schema/watchlist";
|
||||||
|
import { conflictUpdateAllExcept, getColumns } from "~/db/utils";
|
||||||
|
import { KError } from "~/models/error";
|
||||||
|
import { bubble, madeInAbyss } from "~/models/examples";
|
||||||
|
import { Show } from "~/models/show";
|
||||||
|
import {
|
||||||
|
AcceptLanguage,
|
||||||
|
DbMetadata,
|
||||||
|
Filter,
|
||||||
|
Page,
|
||||||
|
createPage,
|
||||||
|
isUuid,
|
||||||
|
processLanguages,
|
||||||
|
} from "~/models/utils";
|
||||||
|
import { desc } from "~/models/utils/descriptions";
|
||||||
|
import { MovieWatchStatus, SerieWatchStatus } from "~/models/watchlist";
|
||||||
|
import { getShows, showFilters, showSort, watchStatusQ } from "./shows/logic";
|
||||||
|
|
||||||
|
async function setWatchStatus({
|
||||||
|
show,
|
||||||
|
status,
|
||||||
|
userId,
|
||||||
|
}: {
|
||||||
|
show: { pk: number; kind: "movie" | "serie" };
|
||||||
|
status: SerieWatchStatus;
|
||||||
|
userId: string;
|
||||||
|
}) {
|
||||||
|
let [profile] = await db
|
||||||
|
.select({ pk: profiles.pk })
|
||||||
|
.from(profiles)
|
||||||
|
.where(eq(profiles.id, userId))
|
||||||
|
.limit(1);
|
||||||
|
if (!profile) {
|
||||||
|
[profile] = await db
|
||||||
|
.insert(profiles)
|
||||||
|
.values({ id: userId })
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
// we can't do `onConflictDoNothing` because on race conditions
|
||||||
|
// we still want the profile to be returned.
|
||||||
|
target: [profiles.id],
|
||||||
|
set: { id: sql`excluded.id` },
|
||||||
|
})
|
||||||
|
.returning({ pk: profiles.pk });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [ret] = await db
|
||||||
|
.insert(watchlist)
|
||||||
|
.values({
|
||||||
|
...status,
|
||||||
|
profilePk: profile.pk,
|
||||||
|
showPk: show.pk,
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: [watchlist.profilePk, watchlist.showPk],
|
||||||
|
set: {
|
||||||
|
...conflictUpdateAllExcept(watchlist, [
|
||||||
|
"profilePk",
|
||||||
|
"showPk",
|
||||||
|
"createdAt",
|
||||||
|
"seenCount",
|
||||||
|
]),
|
||||||
|
// do not reset movie's progress during drop
|
||||||
|
...(show.kind === "movie" && status.status !== "dropped"
|
||||||
|
? { seenCount: sql`excluded.seen_count` }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.returning({
|
||||||
|
...getColumns(watchlist),
|
||||||
|
percent: sql`${watchlist.seenCount}`.as("percent"),
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const watchlistH = new Elysia({ tags: ["profiles"] })
|
||||||
|
.use(auth)
|
||||||
|
.guard(
|
||||||
|
{
|
||||||
|
query: t.Object({
|
||||||
|
sort: showSort,
|
||||||
|
filter: t.Optional(Filter({ def: showFilters })),
|
||||||
|
query: t.Optional(t.String({ description: desc.query })),
|
||||||
|
limit: t.Integer({
|
||||||
|
minimum: 1,
|
||||||
|
maximum: 250,
|
||||||
|
default: 50,
|
||||||
|
description: "Max page size.",
|
||||||
|
}),
|
||||||
|
after: t.Optional(t.String({ description: desc.after })),
|
||||||
|
preferOriginal: t.Optional(
|
||||||
|
t.Boolean({
|
||||||
|
description: desc.preferOriginal,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
(app) =>
|
||||||
|
app
|
||||||
|
.get(
|
||||||
|
"/profiles/me/watchlist",
|
||||||
|
async ({
|
||||||
|
query: { limit, after, query, sort, filter, preferOriginal },
|
||||||
|
headers: { "accept-language": languages },
|
||||||
|
request: { url },
|
||||||
|
jwt: { sub },
|
||||||
|
}) => {
|
||||||
|
const langs = processLanguages(languages);
|
||||||
|
const items = await getShows({
|
||||||
|
limit,
|
||||||
|
after,
|
||||||
|
query,
|
||||||
|
sort,
|
||||||
|
filter: and(
|
||||||
|
isNotNull(watchStatusQ.status),
|
||||||
|
isNull(shows.collectionPk),
|
||||||
|
filter,
|
||||||
|
),
|
||||||
|
languages: langs,
|
||||||
|
preferOriginal,
|
||||||
|
userId: sub,
|
||||||
|
});
|
||||||
|
return createPage(items, { url, sort, limit });
|
||||||
|
},
|
||||||
|
{
|
||||||
|
detail: { description: "Get all movies/series in your watchlist" },
|
||||||
|
headers: t.Object(
|
||||||
|
{
|
||||||
|
"accept-language": AcceptLanguage({ autoFallback: true }),
|
||||||
|
},
|
||||||
|
{ additionalProperties: true },
|
||||||
|
),
|
||||||
|
response: {
|
||||||
|
200: Page(Show),
|
||||||
|
422: KError,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
"/profiles/:id/watchlist",
|
||||||
|
async ({
|
||||||
|
params: { id },
|
||||||
|
query: { limit, after, query, sort, filter, preferOriginal },
|
||||||
|
headers: { "accept-language": languages, authorization },
|
||||||
|
request: { url },
|
||||||
|
error,
|
||||||
|
}) => {
|
||||||
|
const uInfo = await getUserInfo(id, { authorization });
|
||||||
|
|
||||||
|
if ("status" in uInfo) return error(uInfo.status as 404, uInfo);
|
||||||
|
|
||||||
|
const langs = processLanguages(languages);
|
||||||
|
const items = await getShows({
|
||||||
|
limit,
|
||||||
|
after,
|
||||||
|
query,
|
||||||
|
sort,
|
||||||
|
filter: and(
|
||||||
|
isNotNull(watchStatusQ.status),
|
||||||
|
isNull(shows.collectionPk),
|
||||||
|
filter,
|
||||||
|
),
|
||||||
|
languages: langs,
|
||||||
|
preferOriginal,
|
||||||
|
userId: uInfo.id,
|
||||||
|
});
|
||||||
|
return createPage(items, { url, sort, limit });
|
||||||
|
},
|
||||||
|
{
|
||||||
|
detail: {
|
||||||
|
description: "Get all movies/series in someone's watchlist",
|
||||||
|
},
|
||||||
|
params: t.Object({
|
||||||
|
id: t.String({
|
||||||
|
description:
|
||||||
|
"The id or username of the user to read the watchlist of",
|
||||||
|
example: "zoriya",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
headers: t.Object({
|
||||||
|
authorization: t.TemplateLiteral("Bearer ${string}"),
|
||||||
|
"accept-language": AcceptLanguage({ autoFallback: true }),
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: Page(Show),
|
||||||
|
403: KError,
|
||||||
|
404: {
|
||||||
|
...KError,
|
||||||
|
description: "No user found with the specified id/username.",
|
||||||
|
},
|
||||||
|
422: KError,
|
||||||
|
},
|
||||||
|
permissions: ["users.read"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.post(
|
||||||
|
"/series/:id/watchstatus",
|
||||||
|
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({
|
||||||
|
show: { pk: show.pk, kind: "serie" },
|
||||||
|
userId: sub,
|
||||||
|
status: body,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
detail: { description: "Set watchstatus of a series." },
|
||||||
|
params: t.Object({
|
||||||
|
id: t.String({
|
||||||
|
description: "The id or slug of the serie.",
|
||||||
|
example: madeInAbyss.slug,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
body: SerieWatchStatus,
|
||||||
|
response: {
|
||||||
|
200: t.Union([SerieWatchStatus, DbMetadata]),
|
||||||
|
404: KError,
|
||||||
|
},
|
||||||
|
permissions: ["core.read"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.post(
|
||||||
|
"/movies/:id/watchstatus",
|
||||||
|
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({
|
||||||
|
show: { pk: show.pk, kind: "movie" },
|
||||||
|
userId: sub,
|
||||||
|
status: {
|
||||||
|
...body,
|
||||||
|
startedAt: body.completedAt,
|
||||||
|
// for movies, watch-percent is stored in `seenCount`.
|
||||||
|
seenCount: body.status === "completed" ? 100 : 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
detail: { description: "Set watchstatus of a movie." },
|
||||||
|
params: t.Object({
|
||||||
|
id: t.String({
|
||||||
|
description: "The id or slug of the movie.",
|
||||||
|
example: bubble.slug,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
body: t.Omit(MovieWatchStatus, ["percent"]),
|
||||||
|
response: {
|
||||||
|
200: t.Union([MovieWatchStatus, DbMetadata]),
|
||||||
|
404: KError,
|
||||||
|
},
|
||||||
|
permissions: ["core.read"],
|
||||||
|
},
|
||||||
|
);
|
@ -6,9 +6,9 @@ import { shows } from "./shows";
|
|||||||
import { schema } from "./utils";
|
import { schema } from "./utils";
|
||||||
|
|
||||||
export const watchlistStatus = schema.enum("watchlist_status", [
|
export const watchlistStatus = schema.enum("watchlist_status", [
|
||||||
"completed",
|
|
||||||
"watching",
|
"watching",
|
||||||
"rewatching",
|
"rewatching",
|
||||||
|
"completed",
|
||||||
"dropped",
|
"dropped",
|
||||||
"planned",
|
"planned",
|
||||||
]);
|
]);
|
||||||
|
@ -61,6 +61,10 @@ new Elysia()
|
|||||||
name: "images",
|
name: "images",
|
||||||
description: "Routes about images: posters, thumbnails...",
|
description: "Routes about images: posters, thumbnails...",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "profiles",
|
||||||
|
description: "Routes about user profiles, watchlist & history.",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
securitySchemes: {
|
securitySchemes: {
|
||||||
|
@ -16,7 +16,7 @@ import {
|
|||||||
} from "./utils";
|
} from "./utils";
|
||||||
import { Original } from "./utils/original";
|
import { Original } from "./utils/original";
|
||||||
import { EmbeddedVideo } from "./video";
|
import { EmbeddedVideo } from "./video";
|
||||||
import { WatchStatus } from "./watchlist";
|
import { MovieWatchStatus } from "./watchlist";
|
||||||
|
|
||||||
export const MovieStatus = t.UnionEnum(["unknown", "finished", "planned"]);
|
export const MovieStatus = t.UnionEnum(["unknown", "finished", "planned"]);
|
||||||
export type MovieStatus = typeof MovieStatus.static;
|
export type MovieStatus = typeof MovieStatus.static;
|
||||||
@ -56,7 +56,7 @@ export const Movie = t.Intersect([
|
|||||||
t.Object({
|
t.Object({
|
||||||
original: Original,
|
original: Original,
|
||||||
isAvailable: t.Boolean(),
|
isAvailable: t.Boolean(),
|
||||||
watchStatus: t.Nullable(t.Omit(WatchStatus, ["seenCount"])),
|
watchStatus: t.Nullable(MovieWatchStatus),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
export type Movie = Prettify<typeof Movie.static>;
|
export type Movie = Prettify<typeof Movie.static>;
|
||||||
|
@ -17,7 +17,7 @@ import {
|
|||||||
TranslationRecord,
|
TranslationRecord,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
import { Original } from "./utils/original";
|
import { Original } from "./utils/original";
|
||||||
import { WatchStatus } from "./watchlist";
|
import { SerieWatchStatus } from "./watchlist";
|
||||||
|
|
||||||
export const SerieStatus = t.UnionEnum([
|
export const SerieStatus = t.UnionEnum([
|
||||||
"unknown",
|
"unknown",
|
||||||
@ -71,7 +71,7 @@ export const Serie = t.Intersect([
|
|||||||
availableCount: t.Integer({
|
availableCount: t.Integer({
|
||||||
description: "The number of episodes that can be played right away",
|
description: "The number of episodes that can be played right away",
|
||||||
}),
|
}),
|
||||||
watchStatus: t.Nullable(t.Omit(WatchStatus, ["percent"])),
|
watchStatus: t.Nullable(SerieWatchStatus),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
export type Serie = Prettify<typeof Serie.static>;
|
export type Serie = Prettify<typeof Serie.static>;
|
||||||
@ -84,7 +84,7 @@ export const FullSerie = t.Intersect([
|
|||||||
firstEntry: t.Optional(Entry),
|
firstEntry: t.Optional(Entry),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
export type FullMovie = Prettify<typeof FullSerie.static>;
|
export type FullSerie = Prettify<typeof FullSerie.static>;
|
||||||
|
|
||||||
export const SeedSerie = t.Intersect([
|
export const SeedSerie = t.Intersect([
|
||||||
t.Omit(BaseSerie, ["kind", "nextRefresh"]),
|
t.Omit(BaseSerie, ["kind", "nextRefresh"]),
|
||||||
|
@ -36,20 +36,25 @@ export const WatchlistStatus = t.UnionEnum([
|
|||||||
"planned",
|
"planned",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const WatchStatus = t.Object({
|
export const SerieWatchStatus = t.Object({
|
||||||
status: WatchlistStatus,
|
status: WatchlistStatus,
|
||||||
score: t.Nullable(t.Integer({ minimum: 0, maximum: 100 })),
|
score: t.Nullable(t.Integer({ minimum: 0, maximum: 100 })),
|
||||||
startedAt: t.Nullable(t.String({ format: "date-time" })),
|
startedAt: t.Nullable(t.String({ format: "date-time" })),
|
||||||
completedAt: t.Nullable(t.String({ format: "date-time" })),
|
completedAt: t.Nullable(t.String({ format: "date-time" })),
|
||||||
// only for series
|
|
||||||
seenCount: t.Integer({
|
seenCount: t.Integer({
|
||||||
description: "The number of episodes you watched in this serie.",
|
description: "The number of episodes you watched in this serie.",
|
||||||
minimum: 0,
|
minimum: 0,
|
||||||
}),
|
}),
|
||||||
// only for movies
|
|
||||||
percent: t.Integer({
|
|
||||||
minimum: 0,
|
|
||||||
maximum: 100,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
export type WatchStatus = typeof WatchStatus.static;
|
export type SerieWatchStatus = typeof SerieWatchStatus.static;
|
||||||
|
|
||||||
|
export const MovieWatchStatus = t.Intersect([
|
||||||
|
t.Omit(SerieWatchStatus, ["startedAt", "seenCount"]),
|
||||||
|
t.Object({
|
||||||
|
percent: t.Integer({
|
||||||
|
minimum: 0,
|
||||||
|
maximum: 100,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
export type MovieWatchStatus = typeof MovieWatchStatus.static;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export * from "./movies-helper";
|
export * from "./movies-helper";
|
||||||
export * from "./series-helper";
|
export * from "./series-helper";
|
||||||
|
export * from "./shows-helper";
|
||||||
export * from "./studio-helper";
|
export * from "./studio-helper";
|
||||||
export * from "./staff-helper";
|
export * from "./staff-helper";
|
||||||
export * from "./videos-helper";
|
export * from "./videos-helper";
|
||||||
|
@ -5,7 +5,7 @@ export async function getJwtHeaders() {
|
|||||||
sub: "39158be0-3f59-4c45-b00d-d25b3bc2b884",
|
sub: "39158be0-3f59-4c45-b00d-d25b3bc2b884",
|
||||||
sid: "04ac7ecc-255b-481d-b0c8-537c1578e3d5",
|
sid: "04ac7ecc-255b-481d-b0c8-537c1578e3d5",
|
||||||
username: "test-username",
|
username: "test-username",
|
||||||
permissions: ["core.read", "core.write"],
|
permissions: ["core.read", "core.write", "users.read"],
|
||||||
})
|
})
|
||||||
.setProtectedHeader({ alg: "HS256" })
|
.setProtectedHeader({ alg: "HS256" })
|
||||||
.setIssuedAt()
|
.setIssuedAt()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { buildUrl } from "tests/utils";
|
import { buildUrl } from "tests/utils";
|
||||||
import { app } from "~/base";
|
import { app } from "~/base";
|
||||||
import type { SeedMovie } from "~/models/movie";
|
import type { SeedMovie } from "~/models/movie";
|
||||||
|
import type { MovieWatchStatus } from "~/models/watchlist";
|
||||||
import { getJwtHeaders } from "./jwt";
|
import { getJwtHeaders } from "./jwt";
|
||||||
|
|
||||||
export const getMovie = async (
|
export const getMovie = async (
|
||||||
@ -66,3 +67,21 @@ export const createMovie = async (movie: SeedMovie) => {
|
|||||||
const body = await resp.json();
|
const body = await resp.json();
|
||||||
return [resp, body] as const;
|
return [resp, body] as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setMovieStatus = async (
|
||||||
|
id: string,
|
||||||
|
status: Omit<MovieWatchStatus, "percent">,
|
||||||
|
) => {
|
||||||
|
const resp = await app.handle(
|
||||||
|
new Request(buildUrl(`movies/${id}/watchstatus`), {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(status),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(await getJwtHeaders()),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const body = await resp.json();
|
||||||
|
return [resp, body] as const;
|
||||||
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { buildUrl } from "tests/utils";
|
import { buildUrl } from "tests/utils";
|
||||||
import { app } from "~/base";
|
import { app } from "~/base";
|
||||||
import type { SeedSerie } from "~/models/serie";
|
import type { SeedSerie } from "~/models/serie";
|
||||||
|
import type { SerieWatchStatus } from "~/models/watchlist";
|
||||||
import { getJwtHeaders } from "./jwt";
|
import { getJwtHeaders } from "./jwt";
|
||||||
|
|
||||||
export const createSerie = async (serie: SeedSerie) => {
|
export const createSerie = async (serie: SeedSerie) => {
|
||||||
@ -40,6 +41,25 @@ export const getSerie = async (
|
|||||||
return [resp, body] as const;
|
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 (
|
export const getSeasons = async (
|
||||||
serie: string,
|
serie: string,
|
||||||
{
|
{
|
||||||
@ -162,3 +182,18 @@ export const getNews = async ({
|
|||||||
const body = await resp.json();
|
const body = await resp.json();
|
||||||
return [resp, body] as const;
|
return [resp, body] as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setSerieStatus = async (id: string, status: SerieWatchStatus) => {
|
||||||
|
const resp = await app.handle(
|
||||||
|
new Request(buildUrl(`series/${id}/watchstatus`), {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(status),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(await getJwtHeaders()),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const body = await resp.json();
|
||||||
|
return [resp, body] as const;
|
||||||
|
};
|
||||||
|
60
api/tests/helpers/shows-helper.ts
Normal file
60
api/tests/helpers/shows-helper.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { buildUrl } from "tests/utils";
|
||||||
|
import { app } from "~/base";
|
||||||
|
import { getJwtHeaders } from "./jwt";
|
||||||
|
|
||||||
|
export const getShows = async ({
|
||||||
|
langs,
|
||||||
|
...query
|
||||||
|
}: {
|
||||||
|
filter?: string;
|
||||||
|
limit?: number;
|
||||||
|
after?: string;
|
||||||
|
sort?: string | string[];
|
||||||
|
query?: string;
|
||||||
|
langs?: string;
|
||||||
|
preferOriginal?: boolean;
|
||||||
|
}) => {
|
||||||
|
const resp = await app.handle(
|
||||||
|
new Request(buildUrl("shows", query), {
|
||||||
|
method: "GET",
|
||||||
|
headers: langs
|
||||||
|
? {
|
||||||
|
"Accept-Language": langs,
|
||||||
|
...(await getJwtHeaders()),
|
||||||
|
}
|
||||||
|
: await getJwtHeaders(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const body = await resp.json();
|
||||||
|
return [resp, body] as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getWatchlist = async (
|
||||||
|
id: string,
|
||||||
|
{
|
||||||
|
langs,
|
||||||
|
...query
|
||||||
|
}: {
|
||||||
|
filter?: string;
|
||||||
|
limit?: number;
|
||||||
|
after?: string;
|
||||||
|
sort?: string | string[];
|
||||||
|
query?: string;
|
||||||
|
langs?: string;
|
||||||
|
preferOriginal?: boolean;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const resp = await app.handle(
|
||||||
|
new Request(buildUrl(`profiles/${id}/watchlist`, query), {
|
||||||
|
method: "GET",
|
||||||
|
headers: langs
|
||||||
|
? {
|
||||||
|
"Accept-Language": langs,
|
||||||
|
...(await getJwtHeaders()),
|
||||||
|
}
|
||||||
|
: await getJwtHeaders(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const body = await resp.json();
|
||||||
|
return [resp, body] as const;
|
||||||
|
};
|
@ -1,24 +1,29 @@
|
|||||||
import { processImages } from "~/controllers/seed/images";
|
|
||||||
import { db, migrate } from "~/db";
|
import { db, migrate } from "~/db";
|
||||||
import { mqueue, shows, videos } from "~/db/schema";
|
import { profiles, shows } from "~/db/schema";
|
||||||
import { madeInAbyss, madeInAbyssVideo } from "~/models/examples";
|
import { madeInAbyss } from "~/models/examples";
|
||||||
import { createSerie, createVideo, getSerie } from "./helpers";
|
import { createSerie, getSerie, setSerieStatus } from "./helpers";
|
||||||
|
import { getJwtHeaders } from "./helpers/jwt";
|
||||||
|
|
||||||
// test file used to run manually using `bun tests/manual.ts`
|
// test file used to run manually using `bun tests/manual.ts`
|
||||||
|
|
||||||
await migrate();
|
await migrate();
|
||||||
await db.delete(shows);
|
await db.delete(shows);
|
||||||
await db.delete(videos);
|
await db.delete(profiles);
|
||||||
await db.delete(mqueue);
|
|
||||||
|
|
||||||
const [_, vid] = await createVideo(madeInAbyssVideo);
|
console.log(await getJwtHeaders());
|
||||||
console.log(vid);
|
|
||||||
const [__, ser] = await createSerie(madeInAbyss);
|
const [_, ser] = await createSerie(madeInAbyss);
|
||||||
console.log(ser);
|
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, {});
|
||||||
|
console.log(JSON.stringify(got, undefined, 4));
|
||||||
const [___, got] = await getSerie(madeInAbyss.slug, { with: ["translations"] });
|
|
||||||
console.log(got);
|
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
@ -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 { eq } from "drizzle-orm";
|
||||||
import { defaultBlurhash, processImages } from "~/controllers/seed/images";
|
import { defaultBlurhash, processImages } from "~/controllers/seed/images";
|
||||||
import { db } from "~/db";
|
import { db } from "~/db";
|
||||||
@ -6,21 +6,19 @@ import { mqueue, shows, staff, studios, videos } from "~/db/schema";
|
|||||||
import { madeInAbyss } from "~/models/examples";
|
import { madeInAbyss } from "~/models/examples";
|
||||||
import { createSerie } from "../helpers";
|
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", () => {
|
describe("images", () => {
|
||||||
it("Create a serie download images", async () => {
|
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({
|
const ret = await db.query.shows.findFirst({
|
||||||
where: eq(shows.slug, madeInAbyss.slug),
|
where: eq(shows.slug, madeInAbyss.slug),
|
||||||
});
|
});
|
||||||
|
102
api/tests/movies/watchstatus.test.ts
Normal file
102
api/tests/movies/watchstatus.test.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { beforeAll, describe, expect, it } from "bun:test";
|
||||||
|
import {
|
||||||
|
createMovie,
|
||||||
|
getMovie,
|
||||||
|
getShows,
|
||||||
|
getWatchlist,
|
||||||
|
setMovieStatus,
|
||||||
|
} from "tests/helpers";
|
||||||
|
import { expectStatus } from "tests/utils";
|
||||||
|
import { db } from "~/db";
|
||||||
|
import { shows } from "~/db/schema";
|
||||||
|
import { bubble } from "~/models/examples";
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await db.delete(shows);
|
||||||
|
const [ret, body] = await createMovie(bubble);
|
||||||
|
expectStatus(ret, body).toBe(201);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Set & get watch status", () => {
|
||||||
|
it("Creates watchlist entry", async () => {
|
||||||
|
let [resp, body] = await getWatchlist("me", {});
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body.items).toBeArrayOfSize(0);
|
||||||
|
|
||||||
|
const [r, b] = await setMovieStatus(bubble.slug, {
|
||||||
|
status: "completed",
|
||||||
|
completedAt: "2024-12-21",
|
||||||
|
score: 85,
|
||||||
|
});
|
||||||
|
expectStatus(r, b).toBe(200);
|
||||||
|
|
||||||
|
[resp, body] = await getWatchlist("me", {});
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body.items).toBeArrayOfSize(1);
|
||||||
|
expect(body.items[0].slug).toBe(bubble.slug);
|
||||||
|
expect(body.items[0].watchStatus).toMatchObject({
|
||||||
|
status: "completed",
|
||||||
|
completedAt: "2024-12-21 00:00:00+00",
|
||||||
|
score: 85,
|
||||||
|
percent: 100,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Edit watchlist entry", async () => {
|
||||||
|
let [resp, body] = await getWatchlist("me", {});
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body.items).toBeArrayOfSize(1);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
[resp, body] = await getWatchlist("me", {});
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body.items).toBeArrayOfSize(1);
|
||||||
|
expect(body.items[0].slug).toBe(bubble.slug);
|
||||||
|
expect(body.items[0].watchStatus).toMatchObject({
|
||||||
|
status: "rewatching",
|
||||||
|
completedAt: "2024-12-21 00:00:00+00",
|
||||||
|
score: 85,
|
||||||
|
percent: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Return watchstatus in /shows", async () => {
|
||||||
|
const [resp, body] = await getShows({});
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body.items).toBeArrayOfSize(1);
|
||||||
|
expect(body.items[0].slug).toBe(bubble.slug);
|
||||||
|
expect(body.items[0].watchStatus).toMatchObject({
|
||||||
|
status: "rewatching",
|
||||||
|
completedAt: "2024-12-21 00:00:00+00",
|
||||||
|
score: 85,
|
||||||
|
percent: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
expect(body.watchStatus).toMatchObject({
|
||||||
|
status: "rewatching",
|
||||||
|
completedAt: "2024-12-21 00:00:00+00",
|
||||||
|
score: 85,
|
||||||
|
percent: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -11,7 +11,7 @@ RSA_PRIVATE_KEY_PATH=""
|
|||||||
EXTRA_CLAIMS='{}'
|
EXTRA_CLAIMS='{}'
|
||||||
# json object with the claims to add to every jwt of the FIRST user (this can be used to mark the first user as admin).
|
# json object with the claims to add to every jwt of the FIRST user (this can be used to mark the first user as admin).
|
||||||
# Those claims are merged with the `EXTRA_CLAIMS`.
|
# Those claims are merged with the `EXTRA_CLAIMS`.
|
||||||
FIRST_USER_CLAIMS='{}'
|
FIRST_USER_CLAIMS='{"permissions": ["users.read", "users.write", "users.delete"]}'
|
||||||
# If this is not empty, calls to `/jwt` without an `Authorization` header will still create a jwt (with `null` in `sub`)
|
# If this is not empty, calls to `/jwt` without an `Authorization` header will still create a jwt (with `null` in `sub`)
|
||||||
GUEST_CLAIMS=""
|
GUEST_CLAIMS=""
|
||||||
# Comma separated list of claims that users without the `user.write` permissions should NOT be able to edit
|
# Comma separated list of claims that users without the `user.write` permissions should NOT be able to edit
|
||||||
|
@ -47,14 +47,13 @@ func LoadConfiguration(db *dbc.Queries) (*Configuration, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
maps.Insert(ret.FirstUserClaims, maps.All(ret.DefaultClaims))
|
||||||
claims = os.Getenv("FIRST_USER_CLAIMS")
|
claims = os.Getenv("FIRST_USER_CLAIMS")
|
||||||
if claims != "" {
|
if claims != "" {
|
||||||
err := json.Unmarshal([]byte(claims), &ret.FirstUserClaims)
|
err := json.Unmarshal([]byte(claims), &ret.FirstUserClaims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
maps.Insert(ret.FirstUserClaims, maps.All(ret.DefaultClaims))
|
|
||||||
} else {
|
} else {
|
||||||
ret.FirstUserClaims = ret.DefaultClaims
|
ret.FirstUserClaims = ret.DefaultClaims
|
||||||
}
|
}
|
||||||
|
@ -175,10 +175,18 @@ select
|
|||||||
from
|
from
|
||||||
users as u
|
users as u
|
||||||
left join oidc_handle as h on u.pk = h.user_pk
|
left join oidc_handle as h on u.pk = h.user_pk
|
||||||
where
|
where ($1::boolean
|
||||||
u.id = $1
|
and u.id = $2)
|
||||||
|
or (not $1
|
||||||
|
and u.username = $3)
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type GetUserParams struct {
|
||||||
|
UseId bool `json:"useId"`
|
||||||
|
Id uuid.UUID `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
type GetUserRow struct {
|
type GetUserRow struct {
|
||||||
User User `json:"user"`
|
User User `json:"user"`
|
||||||
Provider *string `json:"provider"`
|
Provider *string `json:"provider"`
|
||||||
@ -187,8 +195,8 @@ type GetUserRow struct {
|
|||||||
ProfileUrl *string `json:"profileUrl"`
|
ProfileUrl *string `json:"profileUrl"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetUser(ctx context.Context, id uuid.UUID) ([]GetUserRow, error) {
|
func (q *Queries) GetUser(ctx context.Context, arg GetUserParams) ([]GetUserRow, error) {
|
||||||
rows, err := q.db.Query(ctx, getUser, id)
|
rows, err := q.db.Query(ctx, getUser, arg.UseId, arg.Id, arg.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -28,8 +28,10 @@ select
|
|||||||
from
|
from
|
||||||
users as u
|
users as u
|
||||||
left join oidc_handle as h on u.pk = h.user_pk
|
left join oidc_handle as h on u.pk = h.user_pk
|
||||||
where
|
where (@use_id::boolean
|
||||||
u.id = $1;
|
and u.id = @id)
|
||||||
|
or (not @use_id
|
||||||
|
and u.username = @username);
|
||||||
|
|
||||||
-- name: GetUserByLogin :one
|
-- name: GetUserByLogin :one
|
||||||
select
|
select
|
||||||
|
@ -15,6 +15,15 @@ HTTP 200
|
|||||||
[Captures]
|
[Captures]
|
||||||
jwt: jsonpath "$.token"
|
jwt: jsonpath "$.token"
|
||||||
|
|
||||||
|
GET {{host}}/users/me
|
||||||
|
Authorization: Bearer {{jwt}}
|
||||||
|
HTTP 200
|
||||||
|
[Captures]
|
||||||
|
userId: jsonpath "$.id"
|
||||||
|
[Asserts]
|
||||||
|
# this should be defined in the `FIRST_USER_CLAIMS='{"permissions": ["users.read"]}'` env var
|
||||||
|
jsonpath "$.claims.permissions" contains "users.read"
|
||||||
|
|
||||||
|
|
||||||
# Duplicates usernames
|
# Duplicates usernames
|
||||||
POST {{host}}/users
|
POST {{host}}/users
|
||||||
@ -35,6 +44,26 @@ POST {{host}}/users
|
|||||||
}
|
}
|
||||||
HTTP 409
|
HTTP 409
|
||||||
|
|
||||||
|
# Cannot get non-existing user
|
||||||
|
GET {{host}}/users/dont-exist
|
||||||
|
Authorization: Bearer {{jwt}}
|
||||||
|
HTTP 404
|
||||||
|
|
||||||
|
# Can get user by id
|
||||||
|
GET {{host}}/users/{{userId}}
|
||||||
|
Authorization: Bearer {{jwt}}
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
jsonpath "$.username" == "user-1"
|
||||||
|
|
||||||
|
# Can get user by username
|
||||||
|
GET {{host}}/users/user-1
|
||||||
|
Authorization: Bearer {{jwt}}
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
jsonpath "$.id" == {{userId}}
|
||||||
|
jsonpath "$.username" == "user-1"
|
||||||
|
|
||||||
|
|
||||||
DELETE {{host}}/users/me
|
DELETE {{host}}/users/me
|
||||||
Authorization: Bearer {{jwt}}
|
Authorization: Bearer {{jwt}}
|
||||||
|
@ -94,7 +94,7 @@ func MapOidc(oidc *dbc.GetUserRow) OidcHandle {
|
|||||||
// @Failure 422 {object} KError "Invalid after id"
|
// @Failure 422 {object} KError "Invalid after id"
|
||||||
// @Router /users [get]
|
// @Router /users [get]
|
||||||
func (h *Handler) ListUsers(c echo.Context) error {
|
func (h *Handler) ListUsers(c echo.Context) error {
|
||||||
err := CheckPermissions(c, []string{"user.read"})
|
err := CheckPermissions(c, []string{"users.read"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -139,19 +139,24 @@ func (h *Handler) ListUsers(c echo.Context) error {
|
|||||||
// @Failure 422 {object} KError "Invalid id (not a uuid)"
|
// @Failure 422 {object} KError "Invalid id (not a uuid)"
|
||||||
// @Router /users/{id} [get]
|
// @Router /users/{id} [get]
|
||||||
func (h *Handler) GetUser(c echo.Context) error {
|
func (h *Handler) GetUser(c echo.Context) error {
|
||||||
err := CheckPermissions(c, []string{"user.read"})
|
err := CheckPermissions(c, []string{"users.read"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := uuid.Parse(c.Param("id"))
|
id := c.Param("id")
|
||||||
if err != nil {
|
uid, err := uuid.Parse(c.Param("id"))
|
||||||
return echo.NewHTTPError(http.StatusUnprocessableEntity, "Invalid id")
|
dbuser, err := h.db.GetUser(context.Background(), dbc.GetUserParams{
|
||||||
}
|
UseId: err == nil,
|
||||||
dbuser, err := h.db.GetUser(context.Background(), id)
|
Id: uid,
|
||||||
|
Username: id,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if len(dbuser) == 0 {
|
||||||
|
return echo.NewHTTPError(404, fmt.Sprintf("No user found with id or username: '%s'.", id))
|
||||||
|
}
|
||||||
|
|
||||||
user := MapDbUser(&dbuser[0].User)
|
user := MapDbUser(&dbuser[0].User)
|
||||||
for _, oidc := range dbuser {
|
for _, oidc := range dbuser {
|
||||||
@ -177,7 +182,10 @@ func (h *Handler) GetMe(c echo.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dbuser, err := h.db.GetUser(context.Background(), id)
|
dbuser, err := h.db.GetUser(context.Background(), dbc.GetUserParams{
|
||||||
|
UseId: true,
|
||||||
|
Id: id,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -246,7 +254,7 @@ func (h *Handler) Register(c echo.Context) error {
|
|||||||
// @Failure 404 {object} KError "Invalid user id"
|
// @Failure 404 {object} KError "Invalid user id"
|
||||||
// @Router /users/{id} [delete]
|
// @Router /users/{id} [delete]
|
||||||
func (h *Handler) DeleteUser(c echo.Context) error {
|
func (h *Handler) DeleteUser(c echo.Context) error {
|
||||||
err := CheckPermissions(c, []string{"user.delete"})
|
err := CheckPermissions(c, []string{"users.delete"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -348,7 +356,7 @@ func (h *Handler) EditSelf(c echo.Context) error {
|
|||||||
// @Success 422 {object} KError "Invalid body"
|
// @Success 422 {object} KError "Invalid body"
|
||||||
// @Router /users/{id} [patch]
|
// @Router /users/{id} [patch]
|
||||||
func (h *Handler) EditUser(c echo.Context) error {
|
func (h *Handler) EditUser(c echo.Context) error {
|
||||||
err := CheckPermissions(c, []string{"user.write"})
|
err := CheckPermissions(c, []string{"users.write"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -68,11 +68,21 @@ func CheckPermissions(c echo.Context, perms []string) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return echo.NewHTTPError(403, fmt.Sprintf("Missing permissions: %s.", ", "))
|
return echo.NewHTTPError(403, fmt.Sprintf("Missing permissions: %s.", ", "))
|
||||||
}
|
}
|
||||||
permissions, ok := permissions_claims.([]string)
|
fmt.Printf("%v\n", permissions_claims)
|
||||||
|
fmt.Printf("%t\n", permissions_claims)
|
||||||
|
permissions_int, ok := permissions_claims.([]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return echo.NewHTTPError(403, "Invalid permission claim.")
|
return echo.NewHTTPError(403, "Invalid permission claim.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
permissions := make([]string, len(permissions_int))
|
||||||
|
for i, perm := range permissions_int {
|
||||||
|
permissions[i], ok = perm.(string)
|
||||||
|
if !ok {
|
||||||
|
return echo.NewHTTPError(403, "Invalid permission claim.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
missing := make([]string, 0)
|
missing := make([]string, 0)
|
||||||
for _, perm := range perms {
|
for _, perm := range perms {
|
||||||
if !slices.Contains(permissions, perm) {
|
if !slices.Contains(permissions, perm) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user