Data importing on login and registering

Co-authored-by: William Oldham <github@binaryoverload.co.uk>
This commit is contained in:
mrjvs 2023-11-21 21:26:26 +01:00
parent 7a591c82b9
commit fa29da1757
10 changed files with 239 additions and 41 deletions

View File

@ -3,13 +3,33 @@ import { ofetch } from "ofetch";
import { getAuthHeaders } from "@/backend/accounts/auth";
import { BookmarkResponse } from "@/backend/accounts/user";
import { AccountWithToken } from "@/stores/auth";
import { BookmarkMediaItem } from "@/stores/bookmarks";
export interface BookmarkInput {
export interface BookmarkMetaInput {
title: string;
year: number;
poster?: string;
type: string;
}
export interface BookmarkInput {
tmdbId: string;
meta: BookmarkMetaInput;
}
export function bookmarkMediaToInput(
tmdbId: string,
item: BookmarkMediaItem
): BookmarkInput {
return {
meta: {
title: item.title,
type: item.type,
poster: item.poster,
year: item.year ?? 0,
},
tmdbId,
};
}
export async function addBookmark(
@ -23,10 +43,7 @@ export async function addBookmark(
method: "POST",
headers: getAuthHeaders(account.token),
baseURL: url,
body: {
meta: input,
tmdbId: input.tmdbId,
},
body: input,
}
);
}

View File

@ -0,0 +1,33 @@
import { ofetch } from "ofetch";
import { getAuthHeaders } from "@/backend/accounts/auth";
import { AccountWithToken } from "@/stores/auth";
import { BookmarkInput } from "./bookmarks";
import { ProgressInput } from "./progress";
export function importProgress(
url: string,
account: AccountWithToken,
progressItems: ProgressInput[]
) {
return ofetch<void>(`/users/${account.userId}/progress/import`, {
method: "PUT",
body: progressItems,
baseURL: url,
headers: getAuthHeaders(account.token),
});
}
export function importBookmarks(
url: string,
account: AccountWithToken,
bookmarks: BookmarkInput[]
) {
return ofetch<void>(`/users/${account.userId}/bookmarks`, {
method: "PUT",
body: bookmarks,
baseURL: url,
headers: getAuthHeaders(account.token),
});
}

View File

@ -3,6 +3,7 @@ import { ofetch } from "ofetch";
import { getAuthHeaders } from "@/backend/accounts/auth";
import { ProgressResponse } from "@/backend/accounts/user";
import { AccountWithToken } from "@/stores/auth";
import { ProgressMediaItem, ProgressUpdateItem } from "@/stores/progress";
export interface ProgressInput {
meta?: {
@ -12,14 +13,70 @@ export interface ProgressInput {
type: string;
};
tmdbId: string;
watched?: number;
duration?: number;
watched: number;
duration: number;
seasonId?: string;
episodeId?: string;
seasonNumber?: number;
episodeNumber?: number;
}
export function progressUpdateItemToInput(
item: ProgressUpdateItem
): ProgressInput {
return {
duration: item.progress?.duration ?? 0,
watched: item.progress?.watched ?? 0,
tmdbId: item.tmdbId,
meta: {
title: item.title ?? "",
type: item.type ?? "",
year: item.year ?? NaN,
poster: item.poster,
},
episodeId: item.episodeId,
seasonId: item.seasonId,
episodeNumber: item.episodeNumber,
seasonNumber: item.seasonNumber,
};
}
export function progressMediaItemToInputs(
tmdbId: string,
item: ProgressMediaItem
): ProgressInput[] {
if (item.type === "show") {
return Object.entries(item.episodes).flatMap(([_, episode]) => ({
duration: item.progress?.duration ?? episode.progress.duration,
watched: item.progress?.watched ?? episode.progress.watched,
tmdbId,
meta: {
title: item.title ?? "",
type: item.type ?? "",
year: item.year ?? NaN,
poster: item.poster,
},
episodeId: episode.id,
seasonId: episode.seasonId,
episodeNumber: episode.number,
seasonNumber: item.seasons[episode.seasonId].number,
}));
}
return [
{
duration: item.progress?.duration ?? 0,
watched: item.progress?.watched ?? 0,
tmdbId,
meta: {
title: item.title ?? "",
type: item.type ?? "",
year: item.year ?? NaN,
poster: item.poster,
},
},
];
}
export async function setProgress(
url: string,
account: AccountWithToken,

View File

@ -0,0 +1,37 @@
import { ofetch } from "ofetch";
import { getAuthHeaders } from "@/backend/accounts/auth";
import { AccountWithToken } from "@/stores/auth";
export interface SettingsInput {
applicationLanguage?: string;
applicationTheme?: string;
defaultSubtitleLanguage?: string;
}
export interface SettingsResponse {
applicationTheme?: string | null;
applicationLanguage?: string | null;
defaultSubtitleLanguage?: string | null;
}
export function updateSettings(
url: string,
account: AccountWithToken,
settings: SettingsInput
) {
return ofetch<SettingsResponse>(`/users/${account.userId}/settings`, {
method: "PUT",
body: settings,
baseURL: url,
headers: getAuthHeaders(account.token),
});
}
export function getSettings(url: string, account: AccountWithToken) {
return ofetch<SettingsResponse>(`/users/${account.userId}/settings`, {
method: "GET",
baseURL: url,
headers: getAuthHeaders(account.token),
});
}

View File

@ -1,5 +1,6 @@
import { useCallback } from "react";
import { bookmarkMediaToInput } from "@/backend/accounts/bookmarks";
import {
bytesToBase64,
bytesToBase64Url,
@ -7,7 +8,9 @@ import {
keysFromMnemonic,
signChallenge,
} from "@/backend/accounts/crypto";
import { importBookmarks, importProgress } from "@/backend/accounts/import";
import { getLoginChallengeToken, loginAccount } from "@/backend/accounts/login";
import { progressMediaItemToInputs } from "@/backend/accounts/progress";
import {
getRegisterChallengeToken,
registerAccount,
@ -16,7 +19,9 @@ import { removeSession } from "@/backend/accounts/sessions";
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";
import { AccountWithToken, useAuthStore } from "@/stores/auth";
import { BookmarkMediaItem } from "@/stores/bookmarks";
import { ProgressMediaItem } from "@/stores/progress";
export interface RegistrationData {
recaptchaToken?: string;
@ -69,7 +74,7 @@ export function useAuth() {
const user = await getUser(backendUrl, loginResult.token);
const seedBase64 = bytesToBase64(keys.seed);
await userDataLogin(loginResult, user.user, user.session, seedBase64);
return userDataLogin(loginResult, user.user, user.session, seedBase64);
},
[userDataLogin, backendUrl]
);
@ -106,7 +111,7 @@ export function useAuth() {
profile: registerData.userData.profile,
});
await userDataLogin(
return userDataLogin(
registerResult,
registerResult.user,
registerResult.session,
@ -116,6 +121,33 @@ export function useAuth() {
[backendUrl, userDataLogin]
);
const importData = useCallback(
async (
account: AccountWithToken,
progressItems: Record<string, ProgressMediaItem>,
bookmarks: Record<string, BookmarkMediaItem>
) => {
if (
Object.keys(progressItems).length === 0 &&
Object.keys(bookmarks).length === 0
) {
return;
}
const progressInputs = Object.entries(progressItems).flatMap(
([tmdbId, item]) => progressMediaItemToInputs(tmdbId, item)
);
const bookmarkInputs = Object.entries(bookmarks).map(([tmdbId, item]) =>
bookmarkMediaToInput(tmdbId, item)
);
await importProgress(backendUrl, account, progressInputs);
await importBookmarks(backendUrl, account, bookmarkInputs);
},
[backendUrl]
);
const restore = useCallback(async () => {
if (!currentAccount) {
return;
@ -136,5 +168,6 @@ export function useAuth() {
logout,
register,
restore,
importData,
};
}

View File

@ -24,19 +24,21 @@ export function useAuthData() {
const login = useCallback(
async (
account: LoginResponse,
loginResponse: LoginResponse,
user: UserResponse,
session: SessionResponse,
seed: string
) => {
setAccount({
token: account.token,
const account = {
token: loginResponse.token,
userId: user.id,
sessionId: account.session.id,
sessionId: loginResponse.session.id,
deviceName: session.device,
profile: user.profile,
seed,
});
};
setAccount(account);
return account;
},
[setAccount]
);

View File

@ -11,6 +11,8 @@ import {
} from "@/components/layout/LargeCard";
import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
import { useAuth } from "@/hooks/auth/useAuth";
import { useBookmarkStore } from "@/stores/bookmarks";
import { useProgressStore } from "@/stores/progress";
interface LoginFormPartProps {
onLogin?: () => void;
@ -19,7 +21,9 @@ interface LoginFormPartProps {
export function LoginFormPart(props: LoginFormPartProps) {
const [mnemonic, setMnemonic] = useState("");
const [device, setDevice] = useState("");
const { login, restore } = useAuth();
const { login, restore, importData } = useAuth();
const progressItems = useProgressStore((store) => store.items);
const bookmarkItems = useBookmarkStore((store) => store.bookmarks);
const [result, execute] = useAsyncFn(
async (inputMnemonic: string, inputdevice: string) => {
@ -27,14 +31,14 @@ export function LoginFormPart(props: LoginFormPartProps) {
if (!verifyValidMnemonic(inputMnemonic))
throw new Error("Invalid or incomplete passphrase");
await login({
const account = await login({
mnemonic: inputMnemonic,
userData: {
device: inputdevice,
},
});
// TODO import (and sort out conflicts)
await importData(account, progressItems, bookmarkItems);
await restore();

View File

@ -2,6 +2,7 @@ import { useState } from "react";
import { useGoogleReCaptcha } from "react-google-recaptcha-v3";
import { useAsyncFn } from "react-use";
import { updateSettings } from "@/backend/accounts/settings";
import { Button } from "@/components/Button";
import { Icon, Icons } from "@/components/Icon";
import {
@ -11,7 +12,13 @@ import {
} from "@/components/layout/LargeCard";
import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
import { useAuth } from "@/hooks/auth/useAuth";
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { AccountProfile } from "@/pages/parts/auth/AccountCreatePart";
import { useBookmarkStore } from "@/stores/bookmarks";
import { useLanguageStore } from "@/stores/language";
import { useProgressStore } from "@/stores/progress";
import { useSubtitleStore } from "@/stores/subtitles";
import { useThemeStore } from "@/stores/theme";
interface VerifyPassphraseProps {
mnemonic: string | null;
@ -22,7 +29,17 @@ interface VerifyPassphraseProps {
export function VerifyPassphrase(props: VerifyPassphraseProps) {
const [mnemonic, setMnemonic] = useState("");
const { register, restore } = useAuth();
const { register, restore, importData } = useAuth();
const progressItems = useProgressStore((store) => store.items);
const bookmarkItems = useBookmarkStore((store) => store.bookmarks);
const applicationLanguage = useLanguageStore((store) => store.language);
const defaultSubtitleLanguage = useSubtitleStore(
(store) => store.lastSelectedLanguage
);
const applicationTheme = useThemeStore((store) => store.theme);
const backendUrl = useBackendUrl();
const { executeRecaptcha } = useGoogleReCaptcha();
@ -42,13 +59,19 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
if (inputMnemonic !== props.mnemonic)
throw new Error("Passphrase doesn't match");
await register({
const account = await register({
mnemonic: inputMnemonic,
userData: props.userData,
recaptchaToken,
});
// TODO import (and sort out conflicts)
await importData(account, progressItems, bookmarkItems);
await updateSettings(backendUrl, account, {
applicationLanguage,
defaultSubtitleLanguage: defaultSubtitleLanguage ?? undefined,
applicationTheme: applicationTheme ?? undefined,
});
await restore();

View File

@ -27,11 +27,13 @@ async function syncBookmarks(
if (item.action === "add") {
await addBookmark(url, account, {
meta: {
poster: item.poster,
title: item.title ?? "",
tmdbId: item.tmdbId,
type: item.type ?? "",
year: item.year ?? NaN,
},
tmdbId: item.tmdbId,
});
continue;
}

View File

@ -1,6 +1,10 @@
import { useEffect } from "react";
import { removeProgress, setProgress } from "@/backend/accounts/progress";
import {
progressUpdateItemToInput,
removeProgress,
setProgress,
} from "@/backend/accounts/progress";
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { AccountWithToken, useAuthStore } from "@/stores/auth";
import { ProgressUpdateItem, useProgressStore } from "@/stores/progress";
@ -32,21 +36,7 @@ async function syncProgress(
}
if (item.action === "upsert") {
await setProgress(url, account, {
duration: item.progress?.duration ?? 0,
watched: item.progress?.watched ?? 0,
tmdbId: item.tmdbId,
meta: {
title: item.title ?? "",
type: item.type ?? "",
year: item.year ?? NaN,
poster: item.poster,
},
episodeId: item.episodeId,
seasonId: item.seasonId,
episodeNumber: item.episodeNumber,
seasonNumber: item.seasonNumber,
});
await setProgress(url, account, progressUpdateItemToInput(item));
continue;
}
} catch (err) {