From 5c1a4d4206f003bfc23ec6d425ccb52ff92467fa Mon Sep 17 00:00:00 2001 From: Aleksi Lassila Date: Wed, 12 Jun 2024 18:32:39 +0300 Subject: [PATCH] refactor: User and session management --- backend/src/auth/auth.controller.ts | 8 +- backend/src/auth/auth.service.ts | 2 + src/App.svelte | 17 +- src/lib/apis/jellyfin/jellyfin-api.ts | 12 +- src/lib/apis/radarr/radarr-api.ts | 12 +- src/lib/apis/reiverr/reiverr-api.ts | 9 +- src/lib/apis/reiverr/reiverr.generated.d.ts | 14 +- src/lib/apis/sonarr/sonarr-api.ts | 10 +- src/lib/apis/tmdb/tmdb-api.ts | 8 +- .../DetachedPage/DetachedPage.svelte | 5 +- .../HeroCarousel/HeroBackground.svelte | 4 +- .../Integrations/JellyfinIntegration.svelte | 12 +- .../Integrations/RadarrIntegration.svelte | 8 +- .../Integrations/SonarrIntegration.svelte | 8 +- .../Integrations/TmdbIntegration.svelte | 8 +- .../TmdbIntegrationConnect.svelte | 4 +- src/lib/components/Sidebar/Sidebar.svelte | 246 ++++++++---------- src/lib/components/StackRouter/StackRouter.ts | 16 +- .../JellyfinVideoPlayerModal.svelte | 10 +- .../VideoPlayer/VideoPlayerOld.svelte | 2 +- src/lib/pages/HomePage.svelte | 22 -- src/lib/pages/LoginPage.svelte | 26 +- src/lib/pages/ManagePage.svelte | 19 +- src/lib/pages/OnboardingPage.svelte | 35 +-- src/lib/pages/UsersPage.svelte | 5 + src/lib/stores/app-state.store.ts | 107 -------- src/lib/stores/session.store.ts | 70 +++++ src/lib/stores/user.store.ts | 53 ++++ 28 files changed, 364 insertions(+), 388 deletions(-) delete mode 100644 src/lib/pages/HomePage.svelte create mode 100644 src/lib/pages/UsersPage.svelte delete mode 100644 src/lib/stores/app-state.store.ts create mode 100644 src/lib/stores/session.store.ts create mode 100644 src/lib/stores/user.store.ts diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index 8ab5411..a49e884 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -7,13 +7,15 @@ import { UnauthorizedException, } from '@nestjs/common'; import { AuthService } from './auth.service'; -import { SignInDto } from '../user/user.dto'; +import { SignInDto, UserDto } from '../user/user.dto'; import { ApiOkResponse, ApiProperty } from '@nestjs/swagger'; import { ApiException } from '@nanogiants/nestjs-swagger-api-exception-decorator'; export class SignInResponse { @ApiProperty() accessToken: string; + @ApiProperty() + user: UserDto; } @Controller('auth') @@ -24,12 +26,14 @@ export class AuthController { @ApiOkResponse({ description: 'User found', type: SignInResponse }) @ApiException(() => UnauthorizedException) async signIn(@Body() signInDto: SignInDto) { - const { token } = await this.authService.signIn( + const { token, user } = await this.authService.signIn( signInDto.name, signInDto.password, ); + return { accessToken: token, + user: UserDto.fromEntity(user), }; } } diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index 54a7b75..3e480b4 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -19,6 +19,7 @@ export class AuthService { password: string, ): Promise<{ token: string; + user: User; }> { let user = await this.userService.findOneByName(name); if (!user && (await this.userService.noPreviousAdmins())) @@ -34,6 +35,7 @@ export class AuthService { return { token: await this.jwtService.signAsync(payload), + user, }; } } diff --git a/src/App.svelte b/src/App.svelte index 239f5ae..8ec786e 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -1,22 +1,23 @@ - -
- - - - - - -
diff --git a/src/lib/pages/LoginPage.svelte b/src/lib/pages/LoginPage.svelte index a2782ce..75214c5 100644 --- a/src/lib/pages/LoginPage.svelte +++ b/src/lib/pages/LoginPage.svelte @@ -1,11 +1,13 @@ + +Users Page diff --git a/src/lib/stores/app-state.store.ts b/src/lib/stores/app-state.store.ts deleted file mode 100644 index 0a4b09d..0000000 --- a/src/lib/stores/app-state.store.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { derived, get, writable } from 'svelte/store'; -import { createLocalStorageStore } from './localstorage.store'; -import { - getReiverrApiClient, - reiverrApi, - type ReiverrSettings, - type ReiverrUser -} from '../apis/reiverr/reiverr-api'; - -interface AuthenticationStoreData { - token?: string; - serverBaseUrl?: string; -} - -interface UserStoreData { - user: ReiverrUser | null; -} - -export interface AppStateData extends AuthenticationStoreData { - user: ReiverrUser | null; -} - -const authenticationStore = createLocalStorageStore( - 'authentication-token', - { - token: undefined, - serverBaseUrl: window?.location?.origin - } -); - -function createAppState() { - const userStore = writable(undefined); - - const combinedStore = derived<[typeof userStore, typeof authenticationStore], AppStateData>( - [userStore, authenticationStore], - ([user, auth]) => { - return { - ...user, - ...auth - }; - } - ); - - function setBaseUrl(serverBaseUrl: string | undefined = undefined) { - authenticationStore.update((p) => ({ ...p, serverBaseUrl })); - } - - function setToken(token: string | undefined = undefined) { - authenticationStore.update((p) => ({ ...p, token })); - } - - function setUser(user: ReiverrUser | null) { - userStore.set({ user }); - } - - function logOut() { - setUser(null); - setToken(undefined); - } - - const ready = new Promise((resolve) => { - combinedStore.subscribe((state) => { - if (state.token && state.serverBaseUrl && state.user !== undefined) { - resolve(state); - } - }); - }); - - async function updateUser(updateFn: (user: ReiverrUser) => ReiverrUser) { - const user = get(userStore).user; - - if (!user) return; - - const updated = updateFn(user); - const update = await reiverrApi.updateUser(updated); - - if (update) { - setUser(update); - } - } - - return { - subscribe: combinedStore.subscribe, - setBaseUrl, - setToken, - setUser, - updateUser, - logOut, - ready - }; -} - -export const appState = createAppState(); -export const appStateUser = derived(appState, ($state) => $state.user); - -authenticationStore.subscribe((auth) => { - if (auth.token) { - reiverrApi - .getClient(auth.serverBaseUrl, auth.token) - ?.GET('/user', {}) - .then((res) => res.data) - .then((user) => appState.setUser(user || null)) - .catch((err) => appState.setUser(null)); - } else { - appState.setUser(null); - } -}); diff --git a/src/lib/stores/session.store.ts b/src/lib/stores/session.store.ts new file mode 100644 index 0000000..0671e66 --- /dev/null +++ b/src/lib/stores/session.store.ts @@ -0,0 +1,70 @@ +import { createLocalStorageStore } from './localstorage.store'; +import type { operations } from '../apis/reiverr/reiverr.generated'; +import axios from 'axios'; + +export interface Session { + baseUrl: string; + token: string; +} + +function useSessions() { + const sessions = createLocalStorageStore<{ sessions: Session[]; activeSession?: Session }>( + 'sessions', + { + sessions: [] + } + ); + + function setActiveSession(session: Session) { + sessions.update((s) => ({ ...s, activeSession: session })); + } + + async function addSession(baseUrl: string, name: string, password: string, activate = true) { + const res = await axios + .post( + baseUrl + '/api/auth', + { + name, + password + } + ) + .catch((e) => e.response); + + if (res.status !== 200) return res; + + const session = { + baseUrl, + token: res.data.accessToken + }; + + sessions.update((s) => { + const sessions = s.sessions.concat(session); + return { + sessions, + activeSession: activate ? session : s.activeSession + }; + }); + + return res; + } + + function removeSession(_session?: Session) { + sessions.update((s) => { + const session = _session || s.activeSession; + const sessions = s.sessions.filter((s) => s !== session); + return { + sessions, + activeSession: s.activeSession === session ? undefined : s.activeSession + }; + }); + } + + return { + subscribe: sessions.subscribe, + setActiveSession, + addSession, + removeSession + }; +} + +export const sessions = useSessions(); diff --git a/src/lib/stores/user.store.ts b/src/lib/stores/user.store.ts new file mode 100644 index 0000000..a7cffc3 --- /dev/null +++ b/src/lib/stores/user.store.ts @@ -0,0 +1,53 @@ +import { derived, get, writable } from 'svelte/store'; +import { reiverrApi, type ReiverrUser } from '../apis/reiverr/reiverr-api'; +import axios from 'axios'; +import type { operations } from '../apis/reiverr/reiverr.generated'; +import { type Session, sessions } from './session.store'; + +function useUser() { + const activeSession = derived(sessions, (sessions) => sessions.activeSession); + + const userStore = writable(undefined); + + let lastActiveSession: Session | undefined; + activeSession.subscribe(async (activeSession) => { + if (!activeSession) { + userStore.set(null); + return; + } + + userStore.set(undefined); + lastActiveSession = activeSession; + const user = await axios + .get< + operations['UserController_getProfile']['responses']['200']['content']['application/json'] + >(activeSession.baseUrl + '/api/user', { + headers: { + Authorization: 'Bearer ' + activeSession.token + } + }) + .then((r) => r.data); + + if (lastActiveSession === activeSession) userStore.set(user); + }); + + async function updateUser(updateFn: (user: ReiverrUser) => ReiverrUser) { + const user = get(userStore); + + if (!user) return; + + const updated = updateFn(user); + const update = await reiverrApi.updateUser(updated); + + if (update) { + userStore.set(update); + } + } + + return { + subscribe: userStore.subscribe, + updateUser + }; +} + +export const user = useUser();