feat: Authentication with backend
This commit is contained in:
4
backend/.gitignore
vendored
4
backend/.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
swagger-spec.json
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
@@ -55,4 +57,4 @@ pids
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
*.sqlite
|
||||
*.sqlite
|
||||
|
||||
10
backend/package-lock.json
generated
10
backend/package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "0.0.1",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@nanogiants/nestjs-swagger-api-exception-decorator": "^1.6.11",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
@@ -1610,6 +1611,15 @@
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz",
|
||||
"integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug=="
|
||||
},
|
||||
"node_modules/@nanogiants/nestjs-swagger-api-exception-decorator": {
|
||||
"version": "1.6.11",
|
||||
"resolved": "https://registry.npmjs.org/@nanogiants/nestjs-swagger-api-exception-decorator/-/nestjs-swagger-api-exception-decorator-1.6.11.tgz",
|
||||
"integrity": "sha512-F2Jvj52BDFvKo0I5LFj+kSjwLQecqrs+ibDWokq6Xkod/wrT6gxGia1H/z7ENGk9XwwXfQL9rZt4W/+Vwp0ZhQ==",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^7.6.0 || ^8.0.0 || ^9.0.0 || ^10.0.0",
|
||||
"@nestjs/swagger": "^4.8.1 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/cli": {
|
||||
"version": "10.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.2.tgz",
|
||||
|
||||
@@ -17,9 +17,11 @@
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"openapi:schema": "ts-node src/generate-openapi.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nanogiants/nestjs-swagger-api-exception-decorator": "^1.6.11",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Post,
|
||||
UseGuards,
|
||||
Request,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthGuard } from './auth.guard';
|
||||
import { SignInDto } from '../user/user.dto';
|
||||
import {
|
||||
ApiOkResponse,
|
||||
ApiProperty,
|
||||
ApiUnauthorizedResponse,
|
||||
} from '@nestjs/swagger';
|
||||
import { ApiException } from '@nanogiants/nestjs-swagger-api-exception-decorator';
|
||||
|
||||
export class SignInResponse {
|
||||
@ApiProperty()
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post()
|
||||
async signIn(@Body() signInDto: { name: string; password: string }) {
|
||||
@ApiOkResponse({ description: 'User found', type: SignInResponse })
|
||||
@ApiException(() => UnauthorizedException)
|
||||
async signIn(@Body() signInDto: SignInDto) {
|
||||
const { token } = await this.authService.signIn(
|
||||
signInDto.name,
|
||||
signInDto.password,
|
||||
@@ -25,10 +36,4 @@ export class AuthController {
|
||||
accessToken: token,
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Get('profile')
|
||||
getProfile(@Request() req) {
|
||||
return req.user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,12 @@ import { AccessTokenPayload } from './auth.service';
|
||||
import { User } from '../user/user.entity';
|
||||
import { UserService } from '../user/user.service';
|
||||
|
||||
export const GetUser = createParamDecorator((data, req): User => {
|
||||
return req.user;
|
||||
});
|
||||
export const GetUser = createParamDecorator(
|
||||
(data: unknown, ctx: ExecutionContext): User => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.user;
|
||||
},
|
||||
);
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { JWT_SECRET } from 'src/consts';
|
||||
import { JWT_SECRET } from '../consts';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
||||
@@ -6,14 +6,13 @@ import * as fs from 'fs';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.setGlobalPrefix('api');
|
||||
|
||||
const config = new DocumentBuilder().build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config, {
|
||||
deepScanRoutes: true,
|
||||
});
|
||||
SwaggerModule.setup('openapi', app, document);
|
||||
SwaggerModule.setup('openapi', app, document, {});
|
||||
fs.writeFileSync('./swagger-spec.json', JSON.stringify(document));
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as fs from 'fs';
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.setGlobalPrefix('api');
|
||||
|
||||
app.enableCors();
|
||||
const config = new DocumentBuilder().build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config, {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { AuthGuard, GetUser } from '../auth/auth.guard';
|
||||
import { ApiNotFoundResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { CreateUserDto, UserDto } from './user.dto';
|
||||
import { User } from './user.entity';
|
||||
import { ApiException } from '@nanogiants/nestjs-swagger-api-exception-decorator';
|
||||
|
||||
@ApiTags('user')
|
||||
@Controller('user')
|
||||
@@ -22,9 +23,11 @@ export class UserController {
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Get()
|
||||
@ApiNotFoundResponse({ description: 'User not found' })
|
||||
@ApiOkResponse({ description: 'User found', type: UserDto })
|
||||
@ApiException(() => NotFoundException, { description: 'User not found' })
|
||||
async getProfile(@GetUser() user: User): Promise<UserDto> {
|
||||
console.log(user);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@@ -35,7 +38,7 @@ export class UserController {
|
||||
@UseGuards(AuthGuard)
|
||||
@Get(':id')
|
||||
@ApiOkResponse({ description: 'User found', type: UserDto })
|
||||
@ApiNotFoundResponse({ description: 'User not found' })
|
||||
@ApiException(() => NotFoundException, { description: 'User not found' })
|
||||
async findById(
|
||||
@Param('id') id: string,
|
||||
@GetUser() callerUser: User,
|
||||
|
||||
@@ -19,3 +19,5 @@ export class CreateUserDto extends PickType(User, [
|
||||
] as const) {}
|
||||
|
||||
export class UpdateUserDto extends OmitType(User, ['id'] as const) {}
|
||||
|
||||
export class SignInDto extends PickType(User, ['name', 'password'] as const) {}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UserService } from './user.service';
|
||||
import { userProviders } from './user.providers';
|
||||
import { DatabaseModule } from 'src/database/database.module';
|
||||
import { UserController } from './user.controller';
|
||||
import { DatabaseModule } from '../database/database.module';
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DATA_SOURCE } from 'src/database/database.providers';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
import { DATA_SOURCE } from '../database/database.providers';
|
||||
|
||||
export const USER_REPOSITORY = 'USER_REPOSITORY';
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"openapi":"3.0.0","paths":{"/api/user":{"get":{"operationId":"UserController_getProfile","parameters":[],"responses":{"200":{"description":"User found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserDto"}}}},"404":{"description":"User not found"}},"tags":["user"]},"post":{"operationId":"UserController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"200":{"description":""}},"tags":["user"]}},"/api/user/{id}":{"get":{"operationId":"UserController_findById","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"User found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserDto"}}}},"404":{"description":"User not found"}},"tags":["user"]}},"/api/auth":{"post":{"operationId":"AuthController_signIn","parameters":[],"responses":{"200":{"description":""}}}},"/api/auth/profile":{"get":{"operationId":"AuthController_getProfile","parameters":[],"responses":{"200":{"description":""}}}},"/api":{"get":{"operationId":"AppController_getHello","parameters":[],"responses":{"200":{"description":""}}}}},"info":{"title":"","description":"","version":"1.0.0","contact":{}},"tags":[],"servers":[],"components":{"schemas":{"SonarrSettings":{"type":"object","properties":{"apiKey":{"type":"string"},"baseUrl":{"type":"string"},"qualityProfileId":{"type":"number"},"rootFolderPath":{"type":"string"},"languageProfileId":{"type":"number"}},"required":["apiKey","baseUrl","qualityProfileId","rootFolderPath","languageProfileId"]},"RadarrSettings":{"type":"object","properties":{"apiKey":{"type":"string"},"baseUrl":{"type":"string"},"qualityProfileId":{"type":"number"},"rootFolderPath":{"type":"string"}},"required":["apiKey","baseUrl","qualityProfileId","rootFolderPath"]},"JellyfinSettings":{"type":"object","properties":{"apiKey":{"type":"string"},"baseUrl":{"type":"string"},"userId":{"type":"string"}},"required":["apiKey","baseUrl","userId"]},"Settings":{"type":"object","properties":{"autoplayTrailers":{"type":"boolean"},"language":{"type":"string"},"animationDuration":{"type":"number"},"sonarr":{"$ref":"#/components/schemas/SonarrSettings"},"radarr":{"$ref":"#/components/schemas/RadarrSettings"},"jellyfin":{"$ref":"#/components/schemas/JellyfinSettings"}},"required":["autoplayTrailers","language","animationDuration","sonarr","radarr","jellyfin"]},"UserDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"isAdmin":{"type":"boolean"},"settings":{"$ref":"#/components/schemas/Settings"}},"required":["id","name","isAdmin","settings"]},"CreateUserDto":{"type":"object","properties":{"name":{"type":"string"},"password":{"type":"string"},"isAdmin":{"type":"boolean"}},"required":["name","password","isAdmin"]}}}}
|
||||
@@ -19,7 +19,9 @@
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"test:unit": "vitest",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write ."
|
||||
"format": "prettier --plugin-search-dir . --write .",
|
||||
"openapi:update": "npm run --prefix backend openapi:schema && npm run openapi:codegen",
|
||||
"openapi:codegen": "openapi-typescript \"backend/swagger-spec.json\" -o src/lib/apis/reiverr/reiverr.generated.d.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jellyfin/sdk": "^0.8.2",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import I18n from './lib/components/Lang/I18n.svelte';
|
||||
import { Route, Router } from 'svelte-navigator';
|
||||
import { handleKeyboardNavigation, Selectable } from './lib/selectable';
|
||||
import { onMount } from 'svelte';
|
||||
import { handleKeyboardNavigation } from './lib/selectable';
|
||||
import Container from './Container.svelte';
|
||||
import BrowseSeriesPage from './lib/pages/BrowseSeriesPage.svelte';
|
||||
import MoviesPage from './lib/pages/MoviesPage.svelte';
|
||||
@@ -11,41 +10,57 @@
|
||||
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';
|
||||
|
||||
let mainContent: Selectable;
|
||||
|
||||
onMount(() => {
|
||||
mainContent.focus();
|
||||
});
|
||||
reiverrApi
|
||||
.getApi()
|
||||
.GET('/user', {})
|
||||
.then((res) => res.data)
|
||||
.then((user) => userStore.set(user || null))
|
||||
.catch(() => userStore.set(null));
|
||||
</script>
|
||||
|
||||
<I18n />
|
||||
<Container horizontal class="bg-stone-950 text-white flex flex-1 w-screen">
|
||||
<Router>
|
||||
<Sidebar />
|
||||
{#if $userStore === 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" />
|
||||
<h1 class="font-display uppercase font-semibold tracking-wider text-xl">Reiverr</h1>
|
||||
</div>
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
{:else if $userStore === null}
|
||||
<LoginPage />
|
||||
{:else}
|
||||
<Router>
|
||||
<Sidebar />
|
||||
|
||||
<Container bind:container={mainContent} class="flex-1 flex flex-col min-w-0">
|
||||
<Route path="/">
|
||||
<BrowseSeriesPage />
|
||||
</Route>
|
||||
<Route path="movies">
|
||||
<MoviesPage />
|
||||
</Route>
|
||||
<Route path="library">
|
||||
<LibraryPage />
|
||||
</Route>
|
||||
<Route path="manage">
|
||||
<ManagePage />
|
||||
</Route>
|
||||
<Route path="search">
|
||||
<SearchPage />
|
||||
</Route>
|
||||
<Route path="series/:id" component={SeriesPage} />
|
||||
<Route path="*">
|
||||
<div>404</div>
|
||||
</Route>
|
||||
</Container>
|
||||
</Router>
|
||||
<Container class="flex-1 flex flex-col min-w-0">
|
||||
<Route path="/">
|
||||
<BrowseSeriesPage />
|
||||
</Route>
|
||||
<Route path="movies">
|
||||
<MoviesPage />
|
||||
</Route>
|
||||
<Route path="library">
|
||||
<LibraryPage />
|
||||
</Route>
|
||||
<Route path="manage">
|
||||
<ManagePage />
|
||||
</Route>
|
||||
<Route path="search">
|
||||
<SearchPage />
|
||||
</Route>
|
||||
<Route path="series/:id" component={SeriesPage} />
|
||||
<Route path="*">
|
||||
<div>404</div>
|
||||
</Route>
|
||||
</Container>
|
||||
</Router>
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
<svelte:window on:keydown={handleKeyboardNavigation} />
|
||||
|
||||
11
src/lib/apis/api.interface.ts
Normal file
11
src/lib/apis/api.interface.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type createClient from 'openapi-fetch';
|
||||
|
||||
export abstract class Api<Paths extends NonNullable<unknown>> {
|
||||
protected abstract baseUrl: string;
|
||||
protected abstract client: ReturnType<typeof createClient<Paths>>;
|
||||
protected abstract isLoggedIn: boolean;
|
||||
|
||||
getApi() {
|
||||
return this.client;
|
||||
}
|
||||
}
|
||||
77
src/lib/apis/reiverr/reiverr.generated.d.ts
vendored
77
src/lib/apis/reiverr/reiverr.generated.d.ts
vendored
@@ -5,20 +5,17 @@
|
||||
|
||||
|
||||
export interface paths {
|
||||
"/api/user": {
|
||||
"/user": {
|
||||
get: operations["UserController_getProfile"];
|
||||
post: operations["UserController_create"];
|
||||
};
|
||||
"/api/user/{id}": {
|
||||
"/user/{id}": {
|
||||
get: operations["UserController_findById"];
|
||||
};
|
||||
"/api/auth": {
|
||||
"/auth": {
|
||||
post: operations["AuthController_signIn"];
|
||||
};
|
||||
"/api/auth/profile": {
|
||||
get: operations["AuthController_getProfile"];
|
||||
};
|
||||
"/api": {
|
||||
"/": {
|
||||
get: operations["AppController_getHello"];
|
||||
};
|
||||
}
|
||||
@@ -59,6 +56,18 @@ export interface components {
|
||||
isAdmin: boolean;
|
||||
settings: components["schemas"]["Settings"];
|
||||
};
|
||||
CreateUserDto: {
|
||||
name: string;
|
||||
password: string;
|
||||
isAdmin: boolean;
|
||||
};
|
||||
SignInDto: {
|
||||
name: string;
|
||||
password: string;
|
||||
};
|
||||
SignInResponse: {
|
||||
accessToken: string;
|
||||
};
|
||||
};
|
||||
responses: never;
|
||||
parameters: never;
|
||||
@@ -81,13 +90,26 @@ export interface operations {
|
||||
"application/json": components["schemas"]["UserDto"];
|
||||
};
|
||||
};
|
||||
/** @description User not found */
|
||||
404: {
|
||||
content: never;
|
||||
content: {
|
||||
"application/json": {
|
||||
/** @example 404 */
|
||||
statusCode: number;
|
||||
/** @example Not Found */
|
||||
message: string;
|
||||
/** @example Not Found */
|
||||
error?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
UserController_create: {
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["CreateUserDto"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: {
|
||||
content: never;
|
||||
@@ -107,23 +129,44 @@ export interface operations {
|
||||
"application/json": components["schemas"]["UserDto"];
|
||||
};
|
||||
};
|
||||
/** @description User not found */
|
||||
404: {
|
||||
content: never;
|
||||
content: {
|
||||
"application/json": {
|
||||
/** @example 404 */
|
||||
statusCode: number;
|
||||
/** @example Not Found */
|
||||
message: string;
|
||||
/** @example Not Found */
|
||||
error?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
AuthController_signIn: {
|
||||
responses: {
|
||||
200: {
|
||||
content: never;
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["SignInDto"];
|
||||
};
|
||||
};
|
||||
};
|
||||
AuthController_getProfile: {
|
||||
responses: {
|
||||
/** @description User found */
|
||||
200: {
|
||||
content: never;
|
||||
content: {
|
||||
"application/json": components["schemas"]["SignInResponse"];
|
||||
};
|
||||
};
|
||||
401: {
|
||||
content: {
|
||||
"application/json": {
|
||||
/** @example 401 */
|
||||
statusCode: number;
|
||||
/** @example Unauthorized */
|
||||
message: string;
|
||||
/** @example Unauthorized */
|
||||
error?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
29
src/lib/apis/reiverr/reiverrApi.ts
Normal file
29
src/lib/apis/reiverr/reiverrApi.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
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');
|
||||
48
src/lib/pages/LoginPage.svelte
Normal file
48
src/lib/pages/LoginPage.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { reiverrApi } from '../apis/reiverr/reiverrApi';
|
||||
import { authenticationToken } from '../stores/localstorage.store';
|
||||
|
||||
let name: string;
|
||||
let password: string;
|
||||
let error: string | undefined = undefined;
|
||||
|
||||
function handleLogin() {
|
||||
reiverrApi
|
||||
.getApi()
|
||||
.POST('/auth', {
|
||||
body: {
|
||||
name,
|
||||
password
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.error?.statusCode === 401) {
|
||||
error = 'Invalid credentials. Please try again.';
|
||||
} else if (res.error) {
|
||||
error = res.error.message;
|
||||
} else {
|
||||
const token = res.data.accessToken;
|
||||
authenticationToken.set(token);
|
||||
window.location.reload();
|
||||
}
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
error = err.name + ': ' + err.message;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col">
|
||||
{#if error}
|
||||
<div class="text-red-300">{error}</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
Name: <input class="bg-stone-900" type="text" bind:value={name} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Password: <input class="bg-stone-900" type="password" bind:value={password} />
|
||||
</div>
|
||||
<button on:click={handleLogin}>Submit</button>
|
||||
</div>
|
||||
@@ -13,3 +13,7 @@ export function createLocalStorageStore<T>(key: string, defaultValue: T) {
|
||||
}
|
||||
|
||||
export const skippedVersion = createLocalStorageStore<string | null>('skipped-version', null);
|
||||
export const authenticationToken = createLocalStorageStore<string | null>(
|
||||
'authentication-token',
|
||||
null
|
||||
);
|
||||
|
||||
6
src/lib/stores/user.store.ts
Normal file
6
src/lib/stores/user.store.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
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