diff --git a/src/backend/accounts/auth.ts b/src/backend/accounts/auth.ts index 4084f483..d2caddd9 100644 --- a/src/backend/accounts/auth.ts +++ b/src/backend/accounts/auth.ts @@ -1,5 +1,7 @@ import { ofetch } from "ofetch"; +import { UserResponse } from "@/backend/accounts/user"; + export interface SessionResponse { id: string; userId: string; @@ -8,26 +10,12 @@ export interface SessionResponse { device: string; userAgent: string; } - -export interface UserResponse { - id: string; - namespace: string; - name: string; - roles: string[]; - createdAt: string; - profile: { - colorA: string; - colorB: string; - icon: string; - }; -} - export interface LoginResponse { session: SessionResponse; token: string; } -function getAuthHeaders(token: string): Record { +export function getAuthHeaders(token: string): Record { return { authorization: `Bearer ${token}`, }; @@ -48,16 +36,6 @@ export async function accountLogin( }); } -export async function getUser( - url: string, - token: string -): Promise { - return ofetch("/user/@me", { - headers: getAuthHeaders(token), - baseURL: url, - }); -} - export async function removeSession( url: string, token: string, diff --git a/src/backend/accounts/crypto.ts b/src/backend/accounts/crypto.ts index 31642199..62ba6d3f 100644 --- a/src/backend/accounts/crypto.ts +++ b/src/backend/accounts/crypto.ts @@ -1,6 +1,6 @@ import { pbkdf2Async } from "@noble/hashes/pbkdf2"; import { sha256 } from "@noble/hashes/sha256"; -import { generateMnemonic } from "@scure/bip39"; +import { generateMnemonic, validateMnemonic } from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english"; import forge from "node-forge"; @@ -11,7 +11,11 @@ async function seedFromMnemonic(mnemonic: string) { }); } -export async function keysFromMenmonic(mnemonic: string) { +export function verifyValidMnemonic(mnemonic: string) { + return validateMnemonic(mnemonic, wordlist); +} + +export async function keysFromMnemonic(mnemonic: string) { const seed = await seedFromMnemonic(mnemonic); const { privateKey, publicKey } = forge.pki.ed25519.generateKeyPair({ @@ -45,3 +49,12 @@ export function bytesToBase64Url(bytes: Uint8Array): string { .replace(/\+/g, "-") .replace(/=+$/, ""); } + +export async function signChallenge(mnemonic: string, challengeCode: string) { + const keys = await keysFromMnemonic(mnemonic); + const signature = await signCode(challengeCode, keys.privateKey); + return { + publicKey: bytesToBase64Url(keys.publicKey), + signature: bytesToBase64Url(signature), + }; +} diff --git a/src/backend/accounts/login.ts b/src/backend/accounts/login.ts new file mode 100644 index 00000000..6b18a086 --- /dev/null +++ b/src/backend/accounts/login.ts @@ -0,0 +1,48 @@ +import { ofetch } from "ofetch"; + +import { SessionResponse } from "@/backend/accounts/auth"; + +export interface ChallengeTokenResponse { + challenge: string; +} + +export async function getLoginChallengeToken( + url: string, + publicKey: string +): Promise { + return ofetch("/auth/login/start", { + method: "POST", + body: { + publicKey, + }, + baseURL: url, + }); +} + +export interface LoginResponse { + session: SessionResponse; + token: string; +} + +export interface LoginInput { + publicKey: string; + challenge: { + code: string; + signature: string; + }; + device: string; +} + +export async function loginAccount( + url: string, + data: LoginInput +): Promise { + return ofetch("/auth/login/complete", { + method: "POST", + body: { + namespace: "movie-web", + ...data, + }, + baseURL: url, + }); +} diff --git a/src/backend/accounts/register.ts b/src/backend/accounts/register.ts index 507d4cf9..1adfcd08 100644 --- a/src/backend/accounts/register.ts +++ b/src/backend/accounts/register.ts @@ -1,11 +1,7 @@ import { ofetch } from "ofetch"; -import { SessionResponse, UserResponse } from "@/backend/accounts/auth"; -import { - bytesToBase64Url, - keysFromMenmonic as keysFromMnemonic, - signCode, -} from "@/backend/accounts/crypto"; +import { SessionResponse } from "@/backend/accounts/auth"; +import { UserResponse } from "@/backend/accounts/user"; export interface ChallengeTokenResponse { challenge: string; @@ -57,12 +53,3 @@ export async function registerAccount( baseURL: url, }); } - -export async function signChallenge(mnemonic: string, challengeCode: string) { - const keys = await keysFromMnemonic(mnemonic); - const signature = await signCode(challengeCode, keys.privateKey); - return { - publicKey: bytesToBase64Url(keys.publicKey), - signature: bytesToBase64Url(signature), - }; -} diff --git a/src/backend/accounts/user.ts b/src/backend/accounts/user.ts new file mode 100644 index 00000000..560337f9 --- /dev/null +++ b/src/backend/accounts/user.ts @@ -0,0 +1,128 @@ +import { ofetch } from "ofetch"; + +import { getAuthHeaders } from "@/backend/accounts/auth"; +import { AccountWithToken } from "@/stores/auth"; +import { BookmarkMediaItem } from "@/stores/bookmarks"; +import { ProgressMediaItem } from "@/stores/progress"; + +export interface UserResponse { + id: string; + namespace: string; + name: string; + roles: string[]; + createdAt: string; + profile: { + colorA: string; + colorB: string; + icon: string; + }; +} + +export interface BookmarkResponse { + tmdbId: string; + meta: { + title: string; + year: number; + poster?: string; + type: "show" | "movie"; + }; + updatedAt: string; +} + +export interface ProgressResponse { + tmdbId: string; + seasonId?: string; + episodeId?: string; + meta: { + title: string; + year: number; + poster?: string; + type: "show" | "movie"; + }; + duration: number; + watched: number; + updatedAt: string; +} + +export function bookmarkResponsesToEntries(responses: BookmarkResponse[]) { + const entries = responses.map((bookmark) => { + const item: BookmarkMediaItem = { + ...bookmark.meta, + updatedAt: new Date(bookmark.updatedAt).getTime(), + }; + return [bookmark.tmdbId, item] as const; + }); + + return Object.fromEntries(entries); +} + +export function progressResponsesToEntries(responses: ProgressResponse[]) { + const items: Record = {}; + + responses.forEach((v) => { + if (!items[v.tmdbId]) { + items[v.tmdbId] = { + title: v.meta.title, + poster: v.meta.poster, + type: v.meta.type, + updatedAt: new Date(v.updatedAt).getTime(), + episodes: {}, + seasons: {}, + year: v.meta.year, + }; + } + + const item = items[v.tmdbId]; + if (item.type === "movie") { + item.progress = { + duration: v.duration, + watched: v.watched, + }; + } + + if (item.type === "show" && v.seasonId && v.episodeId) { + item.seasons[v.seasonId] = { + id: v.seasonId, + number: 0, // TODO missing + title: "", // TODO missing + }; + item.episodes[v.episodeId] = { + id: v.seasonId, + number: 0, // TODO missing + title: "", // TODO missing + progress: { + duration: v.duration, + watched: v.watched, + }, + seasonId: v.seasonId, + updatedAt: new Date(v.updatedAt).getTime(), + }; + } + }); + + return items; +} + +export async function getUser( + url: string, + token: string +): Promise { + return ofetch("/users/@me", { + headers: getAuthHeaders(token), + baseURL: url, + }); +} + +export async function getBookmarks(url: string, account: AccountWithToken) { + return ofetch(`/users/${account.userId}/bookmarks`, { + headers: getAuthHeaders(account.token), + baseURL: url, + }); +} + +export async function getProgress(url: string, account: AccountWithToken) { + return ofetch(`/users/${account.userId}/progress`, { + headers: getAuthHeaders(account.token), + baseURL: url, + }); +} diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx new file mode 100644 index 00000000..e05ebb6a --- /dev/null +++ b/src/components/Avatar.tsx @@ -0,0 +1,32 @@ +import { Icon, Icons } from "@/components/Icon"; +import { AccountProfile } from "@/pages/parts/auth/AccountCreatePart"; +import { useAuthStore } from "@/stores/auth"; + +export interface AvatarProps { + profile: AccountProfile["profile"]; +} + +const possibleIcons = ["bookmark"] as const; +const avatarIconMap: Record<(typeof possibleIcons)[number], Icons> = { + bookmark: Icons.BOOKMARK, +}; + +export function Avatar(props: AvatarProps) { + const icon = (avatarIconMap as any)[props.profile.icon] ?? Icons.X; + return ( +
+ +
+ ); +} + +export function UserAvatar() { + const auth = useAuthStore(); + if (!auth.account) return null; + return ; +} diff --git a/src/components/layout/Navigation.tsx b/src/components/layout/Navigation.tsx index f1821ed8..842b89f3 100644 --- a/src/components/layout/Navigation.tsx +++ b/src/components/layout/Navigation.tsx @@ -1,10 +1,11 @@ import classNames from "classnames"; import { Link } from "react-router-dom"; +import { UserAvatar } from "@/components/Avatar"; import { IconPatch } from "@/components/buttons/IconPatch"; import { Icons } from "@/components/Icon"; import { Lightbar } from "@/components/utils/Lightbar"; -import { useAuth } from "@/hooks/useAuth"; +import { useAuth } from "@/hooks/auth/useAuth"; import { BlurEllipsis } from "@/pages/layouts/SubPageLayout"; import { conf } from "@/setup/config"; import { useBannerSize } from "@/stores/banner"; @@ -59,8 +60,8 @@ export function Navigation(props: NavigationProps) { >
-
-
+
+
@@ -81,9 +82,7 @@ export function Navigation(props: NavigationProps) {
-
-

User: {JSON.stringify(loggedIn)}

-
+
{loggedIn ? :

Not logged in

}
diff --git a/src/hooks/auth/useAuth.ts b/src/hooks/auth/useAuth.ts new file mode 100644 index 00000000..8178b355 --- /dev/null +++ b/src/hooks/auth/useAuth.ts @@ -0,0 +1,126 @@ +import { useCallback } from "react"; + +import { removeSession } from "@/backend/accounts/auth"; +import { + bytesToBase64Url, + keysFromMnemonic, + signChallenge, +} from "@/backend/accounts/crypto"; +import { getLoginChallengeToken, loginAccount } from "@/backend/accounts/login"; +import { + getRegisterChallengeToken, + registerAccount, +} from "@/backend/accounts/register"; +import { getBookmarks, getProgress, getUser } from "@/backend/accounts/user"; +import { useAuthData } from "@/hooks/auth/useAuthData"; +import { useBackendUrl } from "@/hooks/auth/useBackendUrl"; +import { useAuthStore } from "@/stores/auth"; + +export interface RegistrationData { + mnemonic: string; + userData: { + device: string; + profile: { + colorA: string; + colorB: string; + icon: string; + }; + }; +} + +export interface LoginData { + mnemonic: string; + userData: { + device: string; + }; +} + +export function useAuth() { + const currentAccount = useAuthStore((s) => s.account); + const profile = useAuthStore((s) => s.account?.profile); + const loggedIn = !!useAuthStore((s) => s.account); + const backendUrl = useBackendUrl(); + const { + logout: userDataLogout, + login: userDataLogin, + syncData, + } = useAuthData(); + + const login = useCallback( + async (loginData: LoginData) => { + const keys = await keysFromMnemonic(loginData.mnemonic); + const { challenge } = await getLoginChallengeToken( + backendUrl, + bytesToBase64Url(keys.publicKey) + ); + const signResult = await signChallenge(loginData.mnemonic, challenge); + const loginResult = await loginAccount(backendUrl, { + challenge: { + code: challenge, + signature: signResult.signature, + }, + publicKey: signResult.publicKey, + device: loginData.userData.device, + }); + + const user = await getUser(backendUrl, loginResult.token); + await userDataLogin(loginResult, user); + }, + [userDataLogin, backendUrl] + ); + + const logout = useCallback(async () => { + if (!currentAccount) return; + try { + await removeSession( + backendUrl, + currentAccount.token, + currentAccount.sessionId + ); + } catch { + // we dont care about failing to delete session + } + userDataLogout(); + }, [userDataLogout, backendUrl, currentAccount]); + + const register = useCallback( + async (registerData: RegistrationData) => { + const { challenge } = await getRegisterChallengeToken(backendUrl); + const signResult = await signChallenge(registerData.mnemonic, challenge); + const registerResult = await registerAccount(backendUrl, { + challenge: { + code: challenge, + signature: signResult.signature, + }, + publicKey: signResult.publicKey, + device: registerData.userData.device, + profile: registerData.userData.profile, + }); + + await userDataLogin(registerResult, registerResult.user); + }, + [backendUrl, userDataLogin] + ); + + const restore = useCallback(async () => { + if (!currentAccount) { + return; + } + + // TODO if fail to get user, log them out + const user = await getUser(backendUrl, currentAccount.token); + const bookmarks = await getBookmarks(backendUrl, currentAccount); + const progress = await getProgress(backendUrl, currentAccount); + + syncData(user, progress, bookmarks); + }, [backendUrl, currentAccount, syncData]); + + return { + loggedIn, + profile, + login, + logout, + register, + restore, + }; +} diff --git a/src/hooks/auth/useAuthData.ts b/src/hooks/auth/useAuthData.ts new file mode 100644 index 00000000..32a58424 --- /dev/null +++ b/src/hooks/auth/useAuthData.ts @@ -0,0 +1,63 @@ +import { useCallback } from "react"; + +import { LoginResponse } from "@/backend/accounts/auth"; +import { + BookmarkResponse, + ProgressResponse, + UserResponse, + bookmarkResponsesToEntries, + progressResponsesToEntries, +} from "@/backend/accounts/user"; +import { useAuthStore } from "@/stores/auth"; +import { useBookmarkStore } from "@/stores/bookmarks"; +import { useProgressStore } from "@/stores/progress"; + +export function useAuthData() { + const loggedIn = !!useAuthStore((s) => s.account); + const setAccount = useAuthStore((s) => s.setAccount); + const removeAccount = useAuthStore((s) => s.removeAccount); + const clearBookmarks = useBookmarkStore((s) => s.clear); + const clearProgress = useProgressStore((s) => s.clear); + + const replaceBookmarks = useBookmarkStore((s) => s.replaceBookmarks); + const replaceItems = useProgressStore((s) => s.replaceItems); + + const login = useCallback( + async (account: LoginResponse, user: UserResponse) => { + setAccount({ + token: account.token, + userId: user.id, + sessionId: account.session.id, + profile: user.profile, + }); + }, + [setAccount] + ); + + const logout = useCallback(async () => { + removeAccount(); + clearBookmarks(); + clearProgress(); + // TODO clear settings + }, [removeAccount, clearBookmarks, clearProgress]); + + const syncData = useCallback( + async ( + _user: UserResponse, + progress: ProgressResponse[], + bookmarks: BookmarkResponse[] + ) => { + // TODO sync user settings + replaceBookmarks(bookmarkResponsesToEntries(bookmarks)); + replaceItems(progressResponsesToEntries(progress)); + }, + [replaceBookmarks, replaceItems] + ); + + return { + loggedIn, + login, + logout, + syncData, + }; +} diff --git a/src/hooks/auth/useAuthRestore.ts b/src/hooks/auth/useAuthRestore.ts new file mode 100644 index 00000000..6f9fe42c --- /dev/null +++ b/src/hooks/auth/useAuthRestore.ts @@ -0,0 +1,16 @@ +import { useAsync, useInterval } from "react-use"; + +import { useAuth } from "@/hooks/auth/useAuth"; + +const AUTH_CHECK_INTERVAL = 12 * 60 * 60 * 1000; + +export function useAuthRestore() { + const { restore } = useAuth(); + + useInterval(() => { + restore(); + }, AUTH_CHECK_INTERVAL); + + const result = useAsync(() => restore(), [restore]); + return result; +} diff --git a/src/hooks/auth/useBackendUrl.ts b/src/hooks/auth/useBackendUrl.ts new file mode 100644 index 00000000..e417ed06 --- /dev/null +++ b/src/hooks/auth/useBackendUrl.ts @@ -0,0 +1,7 @@ +import { conf } from "@/setup/config"; +import { useAuthStore } from "@/stores/auth"; + +export function useBackendUrl() { + const backendUrl = useAuthStore((s) => s.backendUrl); + return backendUrl ?? conf().BACKEND_URL; +} diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts deleted file mode 100644 index 1c87f7ed..00000000 --- a/src/hooks/useAuth.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { useCallback } from "react"; - -import { accountLogin, getUser, removeSession } from "@/backend/accounts/auth"; -import { conf } from "@/setup/config"; -import { useAuthStore } from "@/stores/auth"; - -export function useBackendUrl() { - const backendUrl = useAuthStore((s) => s.backendUrl); - return backendUrl ?? conf().BACKEND_URL; -} - -export function useAuth() { - const currentAccount = useAuthStore((s) => s.account); - const profile = useAuthStore((s) => s.account?.profile); - const loggedIn = !!useAuthStore((s) => s.account); - const setAccount = useAuthStore((s) => s.setAccount); - const removeAccount = useAuthStore((s) => s.removeAccount); - const backendUrl = useBackendUrl(); - - const login = useCallback( - async (id: string, device: string) => { - const account = await accountLogin(backendUrl, id, device); - const user = await getUser(backendUrl, account.token); - setAccount({ - token: account.token, - userId: user.id, - sessionId: account.session.id, - profile: user.profile, - }); - }, - [setAccount, backendUrl] - ); - - const logout = useCallback(async () => { - if (!currentAccount) return; - try { - await removeSession( - backendUrl, - currentAccount.token, - currentAccount.sessionId - ); - } catch { - // we dont care about failing to delete session - } - removeAccount(); // TODO clear local data - }, [removeAccount, backendUrl, currentAccount]); - - return { - loggedIn, - profile, - login, - logout, - }; -} diff --git a/src/index.tsx b/src/index.tsx index ce39d97e..368d7c02 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,12 +1,15 @@ import "core-js/stable"; -import React, { Suspense } from "react"; +import React from "react"; import type { ReactNode } from "react"; import ReactDOM from "react-dom"; import { HelmetProvider } from "react-helmet-async"; import { BrowserRouter, HashRouter } from "react-router-dom"; +import { useAsync } from "react-use"; import { registerSW } from "virtual:pwa-register"; +import { useAuthRestore } from "@/hooks/auth/useAuthRestore"; import { ErrorBoundary } from "@/pages/errors/ErrorBoundary"; +import { MigrationPart } from "@/pages/parts/migrations/MigrationPart"; import App from "@/setup/App"; import { conf } from "@/setup/config"; import i18n from "@/setup/i18n"; @@ -29,13 +32,24 @@ registerSW({ immediate: true, }); -const LazyLoadedApp = React.lazy(async () => { - await initializeOldStores(); - i18n.changeLanguage(useLanguageStore.getState().language); - return { - default: App, - }; -}); +function AuthWrapper() { + const status = useAuthRestore(); + + if (status.loading) return

Fetching user data

; + if (status.error) return

Failed to fetch user data

; + return ; +} + +function MigrationRunner() { + const status = useAsync(async () => { + i18n.changeLanguage(useLanguageStore.getState().language); + await initializeOldStores(); + }, []); + + if (status.loading) return ; + if (status.error) return

Failed to migrate

; + return ; +} function TheRouter(props: { children: ReactNode }) { const normalRouter = conf().NORMAL_ROUTER; @@ -49,9 +63,7 @@ ReactDOM.render( - - - + diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx new file mode 100644 index 00000000..fbf23c50 --- /dev/null +++ b/src/pages/Login.tsx @@ -0,0 +1,18 @@ +import { useHistory } from "react-router-dom"; + +import { SubPageLayout } from "@/pages/layouts/SubPageLayout"; +import { LoginFormPart } from "@/pages/parts/auth/LoginFormPart"; + +export function LoginPage() { + const history = useHistory(); + + return ( + + { + history.push("/"); + }} + /> + + ); +} diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index f7237613..95ff5f2e 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -42,7 +42,7 @@ export function RegisterPage() { {step === 3 ? ( { setStep(4); }} diff --git a/src/pages/parts/auth/LoginFormPart.tsx b/src/pages/parts/auth/LoginFormPart.tsx new file mode 100644 index 00000000..72aabeb0 --- /dev/null +++ b/src/pages/parts/auth/LoginFormPart.tsx @@ -0,0 +1,52 @@ +import { useState } from "react"; +import { useAsyncFn } from "react-use"; + +import { verifyValidMnemonic } from "@/backend/accounts/crypto"; +import { Button } from "@/components/Button"; +import { Input } from "@/components/player/internals/ContextMenu/Input"; +import { useAuth } from "@/hooks/auth/useAuth"; + +interface LoginFormPartProps { + onLogin?: () => void; +} + +export function LoginFormPart(props: LoginFormPartProps) { + const [mnemonic, setMnemonic] = useState(""); + const [device, setDevice] = useState(""); + const { login, restore } = useAuth(); + + const [result, execute] = useAsyncFn( + async (inputMnemonic: string, inputdevice: string) => { + // TODO verify valid device input + if (!verifyValidMnemonic(inputMnemonic)) + throw new Error("Invalid or incomplete passphrase"); + + // TODO captcha? + await login({ + mnemonic: inputMnemonic, + userData: { + device: inputdevice, + }, + }); + + // TODO import (and sort out conflicts) + + await restore(); + + props.onLogin?.(); + }, + [props, login, restore] + ); + + return ( +
+

passphrase

+ +

Device name

+ + {result.loading ?

Loading...

: null} + {result.error ?

error: {result.error.toString()}

: null} + +
+ ); +} diff --git a/src/pages/parts/auth/VerifyPassphrasePart.tsx b/src/pages/parts/auth/VerifyPassphrasePart.tsx index 8d7f5698..3bd58d55 100644 --- a/src/pages/parts/auth/VerifyPassphrasePart.tsx +++ b/src/pages/parts/auth/VerifyPassphrasePart.tsx @@ -1,58 +1,42 @@ import { useState } from "react"; import { useAsyncFn } from "react-use"; -import { - getRegisterChallengeToken, - registerAccount, - signChallenge, -} from "@/backend/accounts/register"; import { Button } from "@/components/Button"; import { Input } from "@/components/player/internals/ContextMenu/Input"; +import { useAuth } from "@/hooks/auth/useAuth"; import { AccountProfile } from "@/pages/parts/auth/AccountCreatePart"; -import { conf } from "@/setup/config"; -import { useAuthStore } from "@/stores/auth"; interface VerifyPassphraseProps { mnemonic: string | null; - profile: AccountProfile | null; + userData: AccountProfile | null; onNext?: () => void; } export function VerifyPassphrase(props: VerifyPassphraseProps) { const [mnemonic, setMnemonic] = useState(""); - const setAccount = useAuthStore((s) => s.setAccount); + const { register, restore } = useAuth(); const [result, execute] = useAsyncFn( async (inputMnemonic: string) => { - if (!props.mnemonic || !props.profile) + if (!props.mnemonic || !props.userData) throw new Error("invalid input data"); if (inputMnemonic !== props.mnemonic) throw new Error("Passphrase doesn't match"); - const url = conf().BACKEND_URL; // TODO captcha? - const { challenge } = await getRegisterChallengeToken(url); - const keys = await signChallenge(inputMnemonic, challenge); - const registerResult = await registerAccount(url, { - challenge: { - code: challenge, - signature: keys.signature, - }, - publicKey: keys.publicKey, - device: props.profile.device, - profile: props.profile.profile, + + await register({ + mnemonic: inputMnemonic, + userData: props.userData, }); - setAccount({ - profile: registerResult.user.profile, - sessionId: registerResult.session.id, - token: registerResult.token, - userId: registerResult.user.id, - }); + // TODO import (and sort out conflicts) + + await restore(); props.onNext?.(); }, - [props, setAccount] + [props, register, restore] ); return ( diff --git a/src/setup/App.tsx b/src/setup/App.tsx index e314484a..81eda406 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -16,6 +16,7 @@ import { AdminPage } from "@/pages/admin/AdminPage"; import { DmcaPage } from "@/pages/Dmca"; import { NotFoundPage } from "@/pages/errors/NotFoundPage"; import { HomePage } from "@/pages/HomePage"; +import { LoginPage } from "@/pages/Login"; import { PlayerView } from "@/pages/PlayerView"; import { RegisterPage } from "@/pages/Register"; import { SettingsPage } from "@/pages/Settings"; @@ -89,6 +90,7 @@ function App() { + diff --git a/src/stores/__old/migrations.ts b/src/stores/__old/migrations.ts index ca00b13f..e79113ca 100644 --- a/src/stores/__old/migrations.ts +++ b/src/stores/__old/migrations.ts @@ -27,7 +27,6 @@ const storeCallbacks: Record void)[]> = {}; const stores: Record, InternalStoreData]> = {}; export async function initializeOldStores() { - console.log(stores); // migrate all stores for (const [store, internal] of Object.values(stores)) { const versions = internal.versions.sort((a, b) => a.version - b.version); @@ -169,7 +168,6 @@ export function createVersionedStore(): StoreBuilder { return this; }, build() { - console.log(_data.key); assertStore(_data); const storageObject = buildStorageObject(_data); stores[_data.key ?? ""] = [storageObject, _data]; diff --git a/src/stores/auth/index.ts b/src/stores/auth/index.ts index d11583b5..9e48d046 100644 --- a/src/stores/auth/index.ts +++ b/src/stores/auth/index.ts @@ -2,7 +2,7 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; import { immer } from "zustand/middleware/immer"; -interface Account { +export interface Account { profile: { colorA: string; colorB: string; @@ -10,7 +10,7 @@ interface Account { }; } -type AccountWithToken = Account & { +export type AccountWithToken = Account & { sessionId: string; userId: string; token: string; diff --git a/src/stores/bookmarks/index.ts b/src/stores/bookmarks/index.ts index d45bfcf9..a4929ac8 100644 --- a/src/stores/bookmarks/index.ts +++ b/src/stores/bookmarks/index.ts @@ -12,16 +12,17 @@ export interface BookmarkMediaItem { updatedAt: number; } -export interface ProgressStore { +export interface BookmarkStore { bookmarks: Record; addBookmark(meta: PlayerMeta): void; removeBookmark(id: string): void; replaceBookmarks(items: Record): void; + clear(): void; } export const useBookmarkStore = create( persist( - immer((set) => ({ + immer((set) => ({ bookmarks: {}, removeBookmark(id) { set((s) => { @@ -44,6 +45,9 @@ export const useBookmarkStore = create( s.bookmarks = items; }); }, + clear() { + this.replaceBookmarks({}); + }, })), { name: "__MW::bookmarks", diff --git a/src/stores/progress/index.ts b/src/stores/progress/index.ts index 5f3e85b5..6c076f9d 100644 --- a/src/stores/progress/index.ts +++ b/src/stores/progress/index.ts @@ -45,6 +45,7 @@ export interface ProgressStore { updateItem(ops: UpdateItemOptions): void; removeItem(id: string): void; replaceItems(items: Record): void; + clear(): void; } export const useProgressStore = create( @@ -111,6 +112,9 @@ export const useProgressStore = create( item.episodes[meta.episode.tmdbId].progress = { ...progress }; }); }, + clear() { + this.replaceItems({}); + }, })), { name: "__MW::progress",