diff --git a/api/src/controllers/seed/index.ts b/api/src/controllers/seed/index.ts index b2208e86..1b72dbf4 100644 --- a/api/src/controllers/seed/index.ts +++ b/api/src/controllers/seed/index.ts @@ -1,8 +1,11 @@ -import Elysia from "elysia"; +import { and, eq } from "drizzle-orm"; +import Elysia, { t } from "elysia"; +import { db } from "~/db"; +import { shows } from "~/db/schema"; import { KError } from "~/models/error"; import { SeedMovie } from "~/models/movie"; import { SeedSerie } from "~/models/serie"; -import { Resource } from "~/models/utils"; +import { isUuid, Resource } from "~/models/utils"; import { comment } from "~/utils"; import { SeedMovieResponse, seedMovie } from "./movies"; import { SeedSerieResponse, seedSerie } from "./series"; @@ -77,4 +80,121 @@ export const seed = new Elysia() 422: KError, }, }, + ) + .delete( + "/movies/:id", + async ({ params: { id }, status }) => { + const [deleted] = await db + .delete(shows) + .where( + and( + eq(shows.kind, "movie"), + isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id), + ), + ) + .returning({ id: shows.id, slug: shows.slug }); + if (!deleted) { + return status(404, { + status: 404, + message: "No movie found with the given id or slug.", + }); + } + return deleted; + }, + { + detail: { + tags: ["movies"], + description: "Delete a movie by id or slug.", + }, + params: t.Object({ + id: t.String({ + description: "The id or slug of the movie to delete.", + }), + }), + response: { + 200: Resource(), + 404: { + ...KError, + description: "No movie found with the given id or slug.", + }, + }, + }, + ) + .delete( + "/series/:id", + async ({ params: { id }, status }) => { + const [deleted] = await db + .delete(shows) + .where( + and( + eq(shows.kind, "serie"), + isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id), + ), + ) + .returning({ id: shows.id, slug: shows.slug }); + if (!deleted) { + return status(404, { + status: 404, + message: "No serie found with the given id or slug.", + }); + } + return deleted; + }, + { + detail: { + tags: ["series"], + description: "Delete a serie by id or slug.", + }, + params: t.Object({ + id: t.String({ + description: "The id or slug of the serie to delete.", + }), + }), + response: { + 200: Resource(), + 404: { + ...KError, + description: "No serie found with the given id or slug.", + }, + }, + }, + ) + .delete( + "/collections/:id", + async ({ params: { id }, status }) => { + const [deleted] = await db + .delete(shows) + .where( + and( + eq(shows.kind, "collection"), + isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id), + ), + ) + .returning({ id: shows.id, slug: shows.slug }); + if (!deleted) { + return status(404, { + status: 404, + message: "No collection found with the given id or slug.", + }); + } + return deleted; + }, + { + detail: { + tags: ["collections"], + description: "Delete a collection by id or slug.", + }, + params: t.Object({ + id: t.String({ + description: "The id or slug of the collection to delete.", + }), + }), + response: { + 200: Resource(), + 404: { + ...KError, + description: "No collection found with the given id or slug.", + }, + }, + }, ); diff --git a/auth/main.go b/auth/main.go index fbb6ccaa..ff89bae2 100644 --- a/auth/main.go +++ b/auth/main.go @@ -3,6 +3,7 @@ package main import ( "context" "encoding/base64" + "errors" "fmt" "log/slog" "net/http" @@ -38,6 +39,7 @@ func ErrorHandler(c *echo.Context, err error) { code := http.StatusInternalServerError var message string + var sc echo.HTTPStatusCoder if he, ok := err.(*echo.HTTPError); ok { code = he.Code @@ -46,8 +48,12 @@ func ErrorHandler(c *echo.Context, err error) { if message == "missing or malformed jwt" { code = http.StatusUnauthorized } + } else if errors.As(err, &sc) { + if tmp := sc.StatusCode(); tmp != 0 { + code = tmp + } } else { - c.Logger().Error(err.Error()) + c.Logger().Error("Unhandled error", slog.Any("err", err)) } c.JSON(code, KError{ @@ -171,14 +177,25 @@ func (h *Handler) TokenToJwt(next echo.HandlerFunc) echo.HandlerFunc { jwt = &token } else { auth := c.Request().Header.Get("Authorization") + var token string - if auth == "" || !strings.HasPrefix(auth, "Bearer ") { + if auth == "" { + cookie, _ := c.Request().Cookie("X-Bearer") + if cookie != nil { + token = cookie.Value + } + } else if strings.HasPrefix(auth, "Bearer ") { + token = auth[len("Bearer "):] + } else if auth != "" { + return echo.NewHTTPError(http.StatusUnauthorized, "Invalid bearer format.") + } + + if token == "" { jwt = h.createGuestJwt() if jwt == nil { return echo.NewHTTPError(http.StatusUnauthorized, "Guests not allowed.") } } else { - token := auth[len("Bearer "):] // this is only used to check if it is a session token or a jwt _, err := base64.RawURLEncoding.DecodeString(token) if err != nil { diff --git a/front/bun.lock b/front/bun.lock index 854d791d..f0689d12 100644 --- a/front/bun.lock +++ b/front/bun.lock @@ -57,7 +57,6 @@ "react-native-worklets": "0.7.2", "react-tooltip": "^5.30.0", "react-use-websocket": "^4.13.0", - "sweetalert2": "^11.26.22", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.1", "tsx": "^4.21.0", @@ -1476,8 +1475,6 @@ "svgo": ["svgo@3.3.3", "", { "dependencies": { "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.0.0", "sax": "^1.5.0" }, "bin": "./bin/svgo" }, "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng=="], - "sweetalert2": ["sweetalert2@11.26.24", "", {}, "sha512-SLgukW4wicewpW5VOukSXY5Z6DL/z7HCOK2ODSjmQPiSphCN8gJAmh9npoceXOtBRNoDN0xIz+zHYthtfiHmjg=="], - "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], "tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="], diff --git a/front/package.json b/front/package.json index 70914f1d..27ec69cc 100644 --- a/front/package.json +++ b/front/package.json @@ -68,7 +68,7 @@ "react-native-worklets": "0.7.2", "react-tooltip": "^5.30.0", "react-use-websocket": "^4.13.0", - "sweetalert2": "^11.26.22", + "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.1", "tsx": "^4.21.0", diff --git a/front/public/translations/en.json b/front/public/translations/en.json index d39cfdfd..608a6f1b 100644 --- a/front/public/translations/en.json +++ b/front/public/translations/en.json @@ -117,6 +117,7 @@ "prev-page": "Previous page", "next-page": "Next page", "delete": "Delete", + "delete-name": "Delete {{name}}", "cancel": "Cancel", "more": "More", "expand": "Expand", diff --git a/front/src/primitives/alert.tsx b/front/src/primitives/alert.tsx index c61d3daf..deadc9a0 100644 --- a/front/src/primitives/alert.tsx +++ b/front/src/primitives/alert.tsx @@ -5,14 +5,13 @@ import { type AlertOptions, Alert as RNAlert, } from "react-native"; -import type { SweetAlertIcon } from "sweetalert2"; export interface ExtendedAlertStatic { alert: ( title: string, message?: string, buttons?: AlertButton[], - options?: AlertOptions & { icon?: SweetAlertIcon }, + options?: AlertOptions, ) => void; } diff --git a/front/src/primitives/alert.web.tsx b/front/src/primitives/alert.web.tsx index 15d96069..cff12eca 100644 --- a/front/src/primitives/alert.web.tsx +++ b/front/src/primitives/alert.web.tsx @@ -1,7 +1,72 @@ -// Stolen from https://github.com/necolas/react-native-web/issues/1026#issuecomment-1458279681 - +import { createRoot } from "react-dom/client"; import type { AlertButton, AlertOptions } from "react-native"; -import Swal, { type SweetAlertIcon } from "sweetalert2"; +import { Pressable, View } from "react-native"; +import { cn } from "~/utils"; +import { Heading, P } from "./text"; + +const AlertDialog = ({ + title, + message, + buttons, + onDismiss, +}: { + title: string; + message?: string; + buttons: AlertButton[]; + onDismiss: () => void; +}) => { + return ( + + e.preventDefault()} + > + + {title} + {message &&

{message}

} +
+ + {buttons.map((button, i) => ( + { + onDismiss(); + button.onPress?.(); + }} + className={cn( + "rounded px-4 py-2", + button.style === "destructive" + ? "bg-red-700 hover:bg-red-500 focus:bg-red-500" + : button.style === "cancel" + ? "hover:bg-popover focus:bg-popover" + : "bg-accent hover:bg-accent/50 focus:bg-accent/50", + )} + > +

+ {button.text ?? "OK"} +

+
+ ))} +
+
+
+ ); +}; // biome-ignore lint/complexity/noStaticOnlyClass: Compatibility with rn export class Alert { @@ -9,42 +74,39 @@ export class Alert { title: string, message?: string, buttons?: AlertButton[], - options?: AlertOptions & { icon?: SweetAlertIcon }, + options?: AlertOptions, ): void { - const confirmButton = buttons - ? buttons.find((button) => button.style === "default") - : undefined; - const denyButton = buttons - ? buttons.find((button) => button.style === "destructive") - : undefined; - const cancelButton = buttons - ? buttons.find((button) => button.style === "cancel") - : undefined; + const resolvedButtons = buttons ?? [ + { text: "OK", style: "default" as const }, + ]; - Swal.fire({ - title: title, - text: message, - showConfirmButton: !!confirmButton, - showDenyButton: !!denyButton, - showCancelButton: !!cancelButton, - confirmButtonText: confirmButton?.text, - denyButtonText: denyButton?.text, - cancelButtonText: cancelButton?.text, - icon: options?.icon, - }).then((result) => { - if (result.isConfirmed) { - if (confirmButton?.onPress !== undefined) { - confirmButton.onPress(); - } - } else if (result.isDenied) { - if (denyButton?.onPress !== undefined) { - denyButton.onPress(); - } - } else if (result.isDismissed) { - if (cancelButton?.onPress !== undefined) { - cancelButton.onPress(); - } - } - }); + const container = document.createElement("div"); + container.style.position = "fixed"; + container.style.inset = "0"; + container.style.zIndex = "9999"; + document.body.appendChild(container); + + const root = createRoot(container); + + const dismiss = () => { + root.unmount(); + container.remove(); + }; + + const cancelButton = resolvedButtons.find((b) => b.style === "cancel"); + + root.render( + { + if (options?.cancelable !== false) { + dismiss(); + cancelButton?.onPress?.(); + } + }} + />, + ); } } diff --git a/front/src/providers/settings.ts b/front/src/providers/settings.ts index 068bf047..52cda316 100644 --- a/front/src/providers/settings.ts +++ b/front/src/providers/settings.ts @@ -3,7 +3,7 @@ import { createMMKV, useMMKVString } from "react-native-mmkv"; import type { ZodType, z } from "zod/v4"; import { getServerData } from "~/utils"; -export const storage = createMMKV(); +export const storage = createMMKV({ id: "kyoo-v5" }); function toBase64(utf8: string) { if (typeof window !== "undefined") return window.btoa(utf8); @@ -35,7 +35,6 @@ export const setCookie = ( export const readCookie = (key: string, parser: T) => { const cookies = getServerData("cookies"); - console.log("cookies", cookies); const decodedCookie = decodeURIComponent(cookies); const ca = decodedCookie.split(";"); diff --git a/front/src/ui/admin/videos-modal/index.tsx b/front/src/ui/admin/videos-modal/index.tsx index 224a5a1e..28bc856e 100644 --- a/front/src/ui/admin/videos-modal/index.tsx +++ b/front/src/ui/admin/videos-modal/index.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { View } from "react-native"; -import { type Entry, FullVideo, type Page } from "~/models"; +import { type Entry, type Episode, FullVideo, type Page } from "~/models"; import { Modal, P } from "~/primitives"; import { InfiniteFetch, @@ -32,17 +32,17 @@ export const useEditLinks = ( body: [ { id: video, - for: entries.map((x) => - x.kind === "episode" && !x.slug - ? { - serie: slug, - // @ts-expect-error: idk why it couldn't match x as an episode - season: x.seasonNumber, - // @ts-expect-error: idk why it couldn't match x as an episode - episode: x.episodeNumber, - } - : { slug: x.slug }, - ), + for: entries.map((x) => { + if (x.slug) return { slug: x.slug }; + const ep = x as Episode; + if (ep.seasonNumber === 0) + return { serie: slug, special: ep.episodeNumber }; + return { + serie: slug, + season: ep.seasonNumber, + episoed: ep.episodeNumber, + }; + }), }, ], }), diff --git a/front/src/ui/details/header.tsx b/front/src/ui/details/header.tsx index 781304f9..dd802d9b 100644 --- a/front/src/ui/details/header.tsx +++ b/front/src/ui/details/header.tsx @@ -1,9 +1,11 @@ import BookmarkAdd from "@material-symbols/svg-400/rounded/bookmark_add.svg"; +import Delete from "@material-symbols/svg-400/rounded/delete.svg"; import MoreHoriz from "@material-symbols/svg-400/rounded/more_horiz.svg"; import MovieInfo from "@material-symbols/svg-400/rounded/movie_info.svg"; import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg"; import Theaters from "@material-symbols/svg-400/rounded/theaters-fill.svg"; import VideoLibrary from "@material-symbols/svg-400/rounded/video_library-fill.svg"; +import { useRouter } from "expo-router"; import { Fragment } from "react"; import { useTranslation } from "react-i18next"; import { View, type ViewProps } from "react-native"; @@ -22,6 +24,7 @@ import { import type { Metadata } from "~/models/utils/metadata"; import { A, + Alert, Chip, Container, capitalize, @@ -45,7 +48,7 @@ import { usePopup, } from "~/primitives"; import { useAccount } from "~/providers/account-context"; -import { Fetch, type QueryIdentifier } from "~/query"; +import { Fetch, type QueryIdentifier, useMutation } from "~/query"; import { cn, displayRuntime, getDisplayDate } from "~/utils"; import { PartOf } from "./part-of"; @@ -72,7 +75,13 @@ const ButtonList = ({ }) => { const account = useAccount(); const { t } = useTranslation(); + const router = useRouter(); const [setPopup, closePopup] = usePopup(); + const deleteMutation = useMutation({ + method: "DELETE", + path: ["api", `${kind}s`, slug], + invalidate: ["api", "shows"], + }); // const metadataRefreshMutation = useMutation({ // method: "POST", @@ -154,6 +163,29 @@ const ButtonList = ({ icon={VideoLibrary} href={`/${kind === "movie" ? "movies" : "series"}/${slug}/videos`} /> + {kind !== "collection" &&
} + { + Alert.alert( + t("misc.delete-name", { name }), + t("login.delete-confirmation"), + [ + { text: t("misc.cancel"), style: "cancel" }, + { + text: t("misc.delete"), + style: "destructive", + onPress: async () => { + await deleteMutation.mutateAsync(); + router.back(); + }, + }, + ], + { cancelable: true }, + ); + }} + /> {/* { { cancelable: true, userInterfaceStyle: theme as "light" | "dark", - icon: "warning", }, ); }}