feat: Implement app state store & fix authentication for tvs
This commit is contained in:
@@ -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>
|
||||
|
||||
25
src/lib/apis/reiverr/reiverr-api.ts
Normal file
25
src/lib/apis/reiverr/reiverr-api.ts
Normal 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;
|
||||
@@ -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');
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
<script lang="ts">
|
||||
import Container from '../../Container.svelte';
|
||||
</script>
|
||||
|
||||
<Container>ManagePage</Container>
|
||||
<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 class="pl-24">
|
||||
<Container on:click={handleLogout} class="hover:bg-red-500">Log Out</Container>
|
||||
</Container>
|
||||
|
||||
@@ -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++;
|
||||
|
||||
50
src/lib/stores/app-state.store.ts
Normal file
50
src/lib/stores/app-state.store.ts
Normal 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);
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user