From db21aef3f3c27ac3892edf9dbe059e2cf606319d Mon Sep 17 00:00:00 2001 From: Aleksi Lassila Date: Sun, 2 Jun 2024 02:59:38 +0300 Subject: [PATCH] feat: Creating the first user --- backend/src/auth/auth.controller.ts | 6 +-- backend/src/auth/auth.guard.ts | 40 ++++++++++++++--- backend/src/auth/auth.module.ts | 2 +- backend/src/auth/auth.service.ts | 5 ++- backend/src/consts.ts | 2 +- backend/src/user/user.controller.ts | 22 ++++++--- src/lib/apis/reiverr/reiverr-api.ts | 5 +++ src/lib/apis/reiverr/reiverr.generated.d.ts | 13 ++++++ src/lib/pages/LoginPage.svelte | 50 ++++++++++++--------- src/lib/pages/ManagePage.svelte | 10 ++++- src/lib/pages/OnboardingPage.svelte | 27 +++++++++-- src/lib/stores/app-state.store.ts | 3 +- 12 files changed, 141 insertions(+), 44 deletions(-) diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index fab1254..8ab5411 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -8,11 +8,7 @@ import { } from '@nestjs/common'; import { AuthService } from './auth.service'; import { SignInDto } from '../user/user.dto'; -import { - ApiOkResponse, - ApiProperty, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; +import { ApiOkResponse, ApiProperty } from '@nestjs/swagger'; import { ApiException } from '@nanogiants/nestjs-swagger-api-exception-decorator'; export class SignInResponse { diff --git a/backend/src/auth/auth.guard.ts b/backend/src/auth/auth.guard.ts index 23d10f6..f51a389 100644 --- a/backend/src/auth/auth.guard.ts +++ b/backend/src/auth/auth.guard.ts @@ -18,6 +18,12 @@ export const GetUser = createParamDecorator( }, ); +function extractTokenFromHeader(request: Request): string | undefined { + const [type, token] = + (request.headers as any).authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; +} + @Injectable() export class AuthGuard implements CanActivate { constructor( @@ -27,7 +33,7 @@ export class AuthGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); - const token = this.extractTokenFromHeader(request); + const token = extractTokenFromHeader(request); if (!token) { throw new UnauthorizedException(); } @@ -46,10 +52,34 @@ export class AuthGuard implements CanActivate { } return true; } +} - private extractTokenFromHeader(request: Request): string | undefined { - const [type, token] = - (request.headers as any).authorization?.split(' ') ?? []; - return type === 'Bearer' ? token : undefined; +@Injectable() +export class OptionalAuthGuard implements CanActivate { + constructor( + private jwtService: JwtService, + private userService: UserService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const token = extractTokenFromHeader(request); + if (!token) { + return true; + } + try { + const payload: AccessTokenPayload = await this.jwtService.verifyAsync( + token, + { + secret: JWT_SECRET, + }, + ); + if (payload.sub) { + request['user'] = await this.userService.findOne(payload.sub); + } + } catch { + return true; + } + return true; } } diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts index 1ba4a24..2c17195 100644 --- a/backend/src/auth/auth.module.ts +++ b/backend/src/auth/auth.module.ts @@ -11,7 +11,7 @@ import { JWT_SECRET } from '../consts'; JwtModule.register({ global: true, secret: JWT_SECRET, - signOptions: { expiresIn: '1d' }, + signOptions: { expiresIn: '1y' }, }), ], controllers: [AuthController], diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index 84340c0..54a7b75 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -1,6 +1,7 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { UserService } from '../user/user.service'; import { JwtService } from '@nestjs/jwt'; +import { User } from '../user/user.entity'; export interface AccessTokenPayload { sub: string; @@ -19,7 +20,9 @@ export class AuthService { ): Promise<{ token: string; }> { - const user = await this.userService.findOneByName(name); + let user = await this.userService.findOneByName(name); + if (!user && (await this.userService.noPreviousAdmins())) + user = await this.userService.create(name, password, true); if (!(user && user.password === password)) { throw new UnauthorizedException(); diff --git a/backend/src/consts.ts b/backend/src/consts.ts index a02381c..37daa38 100644 --- a/backend/src/consts.ts +++ b/backend/src/consts.ts @@ -1 +1 @@ -export const JWT_SECRET = 'secret'; +export const JWT_SECRET = Math.random().toString(36).substring(2, 15); diff --git a/backend/src/user/user.controller.ts b/backend/src/user/user.controller.ts index 03a6a2e..0410583 100644 --- a/backend/src/user/user.controller.ts +++ b/backend/src/user/user.controller.ts @@ -8,13 +8,14 @@ import { Param, Post, Put, + UnauthorizedException, UseGuards, } from '@nestjs/common'; import { UserService } from './user.service'; -import { AuthGuard, GetUser } from '../auth/auth.guard'; -import { ApiNotFoundResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { AuthGuard, GetUser, OptionalAuthGuard } from '../auth/auth.guard'; +import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { CreateUserDto, UpdateUserDto, UserDto } from './user.dto'; -import { Settings, User } from './user.entity'; +import { User } from './user.entity'; import { ApiException } from '@nanogiants/nestjs-swagger-api-exception-decorator'; @ApiTags('user') @@ -57,18 +58,29 @@ export class UserController { return UserDto.fromEntity(user); } + // @Get('isSetupDone') + // @ApiOkResponse({ description: 'Setup done', type: Boolean }) + // async isSetupDone() { + // return this.userService.noPreviousAdmins(); + // } + + @UseGuards(OptionalAuthGuard) @HttpCode(HttpStatus.OK) @Post() async create( @Body() userCreateDto: CreateUserDto, + @GetUser() callerUser: User | undefined, ) { - const canCreateAdmin = await this.userService.noPreviousAdmins(); + const canCreateUser = + (await this.userService.noPreviousAdmins()) || callerUser?.isAdmin; + + if (!canCreateUser) throw new UnauthorizedException(); const user = await this.userService.create( userCreateDto.name, userCreateDto.password, - canCreateAdmin && userCreateDto.isAdmin, + userCreateDto.isAdmin, ); return UserDto.fromEntity(user); diff --git a/src/lib/apis/reiverr/reiverr-api.ts b/src/lib/apis/reiverr/reiverr-api.ts index 823512a..302064a 100644 --- a/src/lib/apis/reiverr/reiverr-api.ts +++ b/src/lib/apis/reiverr/reiverr-api.ts @@ -21,6 +21,11 @@ export class ReiverrApi implements Api { }); } + isSetupDone = async (): Promise => + this.getClient() + ?.GET('/user/isSetupDone') + .then((res) => res.data || false) || false; + async getUser() { const res = await this.getClient()?.GET('/user', {}); return res.data; diff --git a/src/lib/apis/reiverr/reiverr.generated.d.ts b/src/lib/apis/reiverr/reiverr.generated.d.ts index b17f89c..3b4d0e7 100644 --- a/src/lib/apis/reiverr/reiverr.generated.d.ts +++ b/src/lib/apis/reiverr/reiverr.generated.d.ts @@ -13,6 +13,9 @@ export interface paths { get: operations["UserController_findById"]; put: operations["UserController_updateUser"]; }; + "/user/isSetupDone": { + get: operations["UserController_isSetupDone"]; + }; "/auth": { post: operations["AuthController_signIn"]; }; @@ -175,6 +178,16 @@ export interface operations { }; }; }; + UserController_isSetupDone: { + responses: { + /** @description Setup done */ + 200: { + content: { + "application/json": boolean; + }; + }; + }; + }; AuthController_signIn: { requestBody: { content: { diff --git a/src/lib/pages/LoginPage.svelte b/src/lib/pages/LoginPage.svelte index 7a6923f..a2782ce 100644 --- a/src/lib/pages/LoginPage.svelte +++ b/src/lib/pages/LoginPage.svelte @@ -5,8 +5,8 @@ import TextField from '../components/TextField.svelte'; import Button from '../components/Button.svelte'; - let name: string = 'test'; - let password: string = 'test'; + let name: string = ''; + let password: string = ''; let error: string | undefined = undefined; let loading = false; @@ -36,26 +36,34 @@ } - -

Login to Reiverr

+
+ +

Login to Reiverr

+
+ If this is your first time logging in, a new account will be created based on your + credentials. +
- appState.setBaseUrl(e.detail)} - class="mb-4 w-full" - > - Server - + appState.setBaseUrl(e.detail)} + class="mb-4 w-full" + > + Server + - Name - Name + Name + Password - + - {#if error} -
{error}
- {/if} -
+ {#if error} +
{error}
+ {/if} + +
diff --git a/src/lib/pages/ManagePage.svelte b/src/lib/pages/ManagePage.svelte index 747f9c3..78662f9 100644 --- a/src/lib/pages/ManagePage.svelte +++ b/src/lib/pages/ManagePage.svelte @@ -145,7 +145,7 @@ 'text-primary-500': hasFocus })} > - Interface + General
({ ...p, useCssTransitions: detail }))} /> +
+ + + localSettings.update((p) => ({ ...p, checkForUpdates: detail }))} + /> +
diff --git a/src/lib/pages/OnboardingPage.svelte b/src/lib/pages/OnboardingPage.svelte index dfee075..dd7a8aa 100644 --- a/src/lib/pages/OnboardingPage.svelte +++ b/src/lib/pages/OnboardingPage.svelte @@ -4,7 +4,7 @@ import Button from '../components/Button.svelte'; import { appState } from '../stores/app-state.store'; import { tmdbApi } from '../apis/tmdb/tmdb-api'; - import { ArrowRight, ExternalLink } from 'radix-icons-svelte'; + import { ArrowLeft, ArrowRight, CheckCircled, ExternalLink } from 'radix-icons-svelte'; import TextField from '../components/TextField.svelte'; import { jellyfinApi, type JellyfinUser } from '../apis/jellyfin/jellyfin-api'; import SelectField from '../components/SelectField.svelte'; @@ -13,6 +13,7 @@ import { radarrApi } from '../apis/radarr/radarr-api'; import { get } from 'svelte/store'; import { useTabs } from '../components/Tab/Tab'; + import classNames from 'classnames'; enum Tabs { Welcome, @@ -20,6 +21,7 @@ Jellyfin, Sonarr, Radarr, + Complete, SelectUser = Jellyfin + 0.1, TmdbConnect = Tmdb + 0.1 @@ -123,7 +125,7 @@ } })); - tab.next(); + tab.set(Tabs.Jellyfin); }); } @@ -409,8 +411,27 @@ {#if radarrBaseUrl && radarrApiKey} {:else} - + {/if}
+ + +
+ +
+

All Set!

+
Reiverr is now ready to use.
+ + + +
+ +
+
+
diff --git a/src/lib/stores/app-state.store.ts b/src/lib/stores/app-state.store.ts index 0aef49e..0a4b09d 100644 --- a/src/lib/stores/app-state.store.ts +++ b/src/lib/stores/app-state.store.ts @@ -95,7 +95,8 @@ export const appStateUser = derived(appState, ($state) => $state.user); authenticationStore.subscribe((auth) => { if (auth.token) { - getReiverrApiClient(auth.serverBaseUrl, auth.token) + reiverrApi + .getClient(auth.serverBaseUrl, auth.token) ?.GET('/user', {}) .then((res) => res.data) .then((user) => appState.setUser(user || null))