feat: Implement app state store & fix authentication for tvs

This commit is contained in:
Aleksi Lassila
2024-03-27 17:22:19 +02:00
parent a574b718f0
commit da2b4ee6d5
10 changed files with 153 additions and 74 deletions

View File

@@ -10,21 +10,20 @@
import SearchPage from './lib/pages/SearchPage.svelte';
import SeriesPage from './lib/pages/SeriesPage.svelte';
import Sidebar from './lib/components/Sidebar/Sidebar.svelte';
import { userStore } from './lib/stores/user.store';
import LoginPage from './lib/pages/LoginPage.svelte';
import { reiverrApi } from './lib/apis/reiverr/reiverrApi';
import { getReiverrApiClient } from './lib/apis/reiverr/reiverr-api';
import { appState } from './lib/stores/app-state.store';
reiverrApi
.getApi()
getReiverrApiClient()
.GET('/user', {})
.then((res) => res.data)
.then((user) => userStore.set(user || null))
.catch(() => userStore.set(null));
.then((user) => appState.setUser(user || null))
.catch(() => appState.setUser(null));
</script>
<I18n />
<Container horizontal class="bg-stone-950 text-white flex flex-1 w-screen">
{#if $userStore === undefined}
{#if $appState.user === undefined}
<div class="h-screen w-screen flex flex-col items-center justify-center">
<div class="flex items-center justify-center hover:text-inherit selectable rounded-sm mb-2">
<div class="rounded-full bg-amber-300 h-4 w-4 mr-2" />
@@ -32,7 +31,7 @@
</div>
<div>Loading...</div>
</div>
{:else if $userStore === null}
{:else if $appState.user === null}
<LoginPage />
{:else}
<Router>

View File

@@ -0,0 +1,25 @@
import createClient from 'openapi-fetch';
import type { paths } from './reiverr.generated';
import { get } from 'svelte/store';
import { appState } from '../../stores/app-state.store';
interface ApiInterface<Paths extends NonNullable<unknown>> {
getClient(): ReturnType<typeof createClient<Paths>>;
}
export class ReiverrApi implements ApiInterface<paths> {
getClient(basePath?: string) {
const token = get(appState).token;
return createClient<paths>({
baseUrl: (basePath || get(appState).serverBaseUrl) + '/api',
...(token && {
headers: {
Authorization: 'Bearer ' + token
}
})
});
}
}
export const reiverrApi = new ReiverrApi();
export const getReiverrApiClient = reiverrApi.getClient;

View File

@@ -1,29 +0,0 @@
import createClient from 'openapi-fetch';
import type { paths } from './reiverr.generated';
import { Api } from '../api.interface';
import { authenticationToken } from '../../stores/localstorage.store';
import { get } from 'svelte/store';
class ReiverrApi<asd extends NonNullable<unknown>> extends Api<asd> {
protected baseUrl: string;
protected client: ReturnType<typeof createClient<paths>>;
protected isLoggedIn = false;
constructor(baseUrl: string) {
super();
this.baseUrl = baseUrl;
const token = get(authenticationToken);
this.client = createClient<paths>({
baseUrl: this.baseUrl,
...(token && {
headers: {
Authorization: 'Bearer ' + token
}
})
});
}
}
export const reiverrApi = new ReiverrApi<paths>('http://localhost:3000/api');

View File

@@ -13,7 +13,7 @@
export let items: Promise<ShowcaseItemProps[]> = Promise.resolve([]);
let showcaseIndex = 6;
let showcaseIndex = 0;
let showcaseLength = 0;
$: items.then((i) => (showcaseLength = i?.length || 0));

View File

@@ -1,14 +1,18 @@
<script lang="ts">
import { reiverrApi } from '../apis/reiverr/reiverrApi';
import { authenticationToken } from '../stores/localstorage.store';
import { getReiverrApiClient } from '../apis/reiverr/reiverr-api';
import Container from '../../Container.svelte';
import { appState } from '../stores/app-state.store';
let name: string;
let password: string;
let name: string = 'test';
let password: string = 'test';
let error: string | undefined = undefined;
let input0: HTMLInputElement;
let input1: HTMLInputElement;
let input2: HTMLInputElement;
function handleLogin() {
reiverrApi
.getApi()
getReiverrApiClient()
.POST('/auth', {
body: {
name,
@@ -19,10 +23,10 @@
if (res.error?.statusCode === 401) {
error = 'Invalid credentials. Please try again.';
} else if (res.error) {
error = res.error.message;
error = 'Error occurred: ' + res.error.message;
} else {
const token = res.data.accessToken;
authenticationToken.set(token);
appState.setToken(token);
window.location.reload();
}
})
@@ -32,17 +36,36 @@
}
</script>
<div class="flex flex-col">
<Container class="flex flex-col" focusOnMount>
{#if error}
<div class="text-red-300">{error}</div>
{/if}
<div>
Name: <input class="bg-stone-900" type="text" bind:value={name} />
Server:
<Container on:click={() => input0?.focus()}>
<input
class="bg-stone-900"
type="text"
value={$appState.serverBaseUrl}
on:change={(e) => appState.setBaseUrl(e?.target?.value)}
bind:this={input0}
/>
</Container>
</div>
<div>
Password: <input class="bg-stone-900" type="password" bind:value={password} />
Name:
<Container on:click={() => input1?.focus()}>
<input class="bg-stone-900" type="text" bind:value={name} bind:this={input1} />
</Container>
</div>
<button on:click={handleLogin}>Submit</button>
<div>
Password:
<Container on:click={() => input2?.focus()}>
<input class="bg-stone-900" type="password" bind:value={password} bind:this={input2} />
</Container>
</div>
<Container on:click={handleLogin}>Submit</Container>
</Container>

View File

@@ -1,5 +1,17 @@
<script lang="ts">
import Container from '../../Container.svelte';
import { appState } from '../stores/app-state.store';
import { useNavigate } from 'svelte-navigator';
const navigate = useNavigate();
function handleLogout() {
console.log('Hello world');
appState.setToken(undefined);
// window.location.replace('/');
window.location.reload();
}
</script>
<Container>ManagePage</Container>
<Container class="pl-24">
<Container on:click={handleLogout} class="hover:bg-red-500">Log Out</Container>
</Container>

View File

@@ -84,20 +84,20 @@ export class Selectable {
const focusIndex = get(this.focusIndex);
if (this.children[focusIndex]?.isFocusable()) {
this.children[focusIndex].focus();
this.children[focusIndex]?.focus();
} else {
let i = focusIndex;
while (i < this.children.length) {
if (this.children[i].isFocusable()) {
this.children[i].focus();
if (this.children[i]?.isFocusable()) {
this.children[i]?.focus();
return;
}
i++;
}
i = focusIndex - 1;
while (i >= 0) {
if (this.children[i].isFocusable()) {
this.children[i].focus();
if (this.children[i]?.isFocusable()) {
this.children[i]?.focus();
return;
}
i--;
@@ -163,7 +163,7 @@ export class Selectable {
if (direction === 'up' || direction === 'left') {
let index = focusIndex - 1;
while (index >= 0) {
if (this.children[index].isFocusable()) {
if (this.children[index]?.isFocusable()) {
return this.children[index];
}
index--;
@@ -171,7 +171,7 @@ export class Selectable {
} else if (direction === 'down' || direction === 'right') {
let index = focusIndex + 1;
while (index < this.children.length) {
if (this.children[index].isFocusable()) {
if (this.children[index]?.isFocusable()) {
return this.children[index];
}
index++;

View File

@@ -0,0 +1,50 @@
import { derived, writable } from 'svelte/store';
import type { components } from '../apis/reiverr/reiverr.generated';
import { createLocalStorageStore } from './localstorage.store';
export type User = components['schemas']['UserDto'];
interface AuthenticationStoreData {
token?: string;
serverBaseUrl?: string;
}
function createAppState() {
const userStore = writable<User | null>(undefined);
const authenticationStore = createLocalStorageStore<AuthenticationStoreData>(
'authentication-token',
{
token: undefined,
serverBaseUrl: window?.location?.origin
}
);
const combinedStore = derived([userStore, authenticationStore], ([$user, $auth]) => {
return {
user: $user,
token: $auth.token,
serverBaseUrl: $auth.serverBaseUrl
};
});
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: User | null) {
userStore.set(user);
}
return {
subscribe: combinedStore.subscribe,
setBaseUrl,
setToken,
setUser
};
}
export const appState = createAppState();
export const appStateUser = derived(appState, ($state) => $state.user);

View File

@@ -1,4 +1,4 @@
import { writable } from 'svelte/store';
import { get, writable } from 'svelte/store';
export function createLocalStorageStore<T>(key: string, defaultValue: T) {
const store = writable<T>(JSON.parse(localStorage.getItem(key) || 'null') || defaultValue);
@@ -8,12 +8,17 @@ export function createLocalStorageStore<T>(key: string, defaultValue: T) {
set: (value: T) => {
localStorage.setItem(key, JSON.stringify(value));
store.set(value);
},
update: (updater: (value: T) => T) => {
const newValue = updater(get(store));
localStorage.setItem(key, JSON.stringify(newValue));
store.set(newValue);
},
remove: () => {
localStorage.removeItem(key);
store.set(defaultValue);
}
};
}
export const skippedVersion = createLocalStorageStore<string | null>('skipped-version', null);
export const authenticationToken = createLocalStorageStore<string | null>(
'authentication-token',
null
);

View File

@@ -1,6 +0,0 @@
import { writable } from 'svelte/store';
import type { components } from '../apis/reiverr/reiverr.generated';
export type User = components['schemas']['UserDto'];
export const userStore = writable<User | null>(undefined);