feat: Creating the first user
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<boolean> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { JWT_SECRET } from '../consts';
|
||||
JwtModule.register({
|
||||
global: true,
|
||||
secret: JWT_SECRET,
|
||||
signOptions: { expiresIn: '1d' },
|
||||
signOptions: { expiresIn: '1y' },
|
||||
}),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const JWT_SECRET = 'secret';
|
||||
export const JWT_SECRET = Math.random().toString(36).substring(2, 15);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -21,6 +21,11 @@ export class ReiverrApi implements Api<paths> {
|
||||
});
|
||||
}
|
||||
|
||||
isSetupDone = async (): Promise<boolean> =>
|
||||
this.getClient()
|
||||
?.GET('/user/isSetupDone')
|
||||
.then((res) => res.data || false) || false;
|
||||
|
||||
async getUser() {
|
||||
const res = await this.getClient()?.GET('/user', {});
|
||||
return res.data;
|
||||
|
||||
13
src/lib/apis/reiverr/reiverr.generated.d.ts
vendored
13
src/lib/apis/reiverr/reiverr.generated.d.ts
vendored
@@ -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: {
|
||||
|
||||
@@ -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 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container
|
||||
class="w-full h-full max-w-xs mx-auto flex flex-col items-center justify-center"
|
||||
focusOnMount
|
||||
>
|
||||
<h1 class="font-semibold tracking-wide text-xl w-full mb-4">Login to Reiverr</h1>
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<Container class="flex flex-col bg-primary-800 rounded-2xl p-10 shadow-xl max-w-lg" focusOnMount>
|
||||
<h1 class="header2 w-full mb-2">Login to Reiverr</h1>
|
||||
<div class="header1 mb-4">
|
||||
If this is your first time logging in, a new account will be created based on your
|
||||
credentials.
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
value={$appState.serverBaseUrl}
|
||||
on:change={(e) => appState.setBaseUrl(e.detail)}
|
||||
class="mb-4 w-full"
|
||||
>
|
||||
Server
|
||||
</TextField>
|
||||
<TextField
|
||||
value={$appState.serverBaseUrl}
|
||||
on:change={(e) => appState.setBaseUrl(e.detail)}
|
||||
class="mb-4 w-full"
|
||||
>
|
||||
Server
|
||||
</TextField>
|
||||
|
||||
<TextField bind:value={name} class="mb-4 w-full">Name</TextField>
|
||||
<TextField bind:value={password} type="password" class="mb-8 w-full">Name</TextField>
|
||||
<TextField bind:value={name} class="mb-4 w-full">Name</TextField>
|
||||
<TextField bind:value={password} type="password" class="mb-8 w-full">Password</TextField>
|
||||
|
||||
<Button disabled={loading} on:clickOrSelect={handleLogin} class="mb-4 w-full">Submit</Button>
|
||||
<Button
|
||||
type="primary-dark"
|
||||
disabled={loading}
|
||||
on:clickOrSelect={handleLogin}
|
||||
class="mb-4 w-full">Submit</Button
|
||||
>
|
||||
|
||||
{#if error}
|
||||
<div class="text-red-300 text-center">{error}</div>
|
||||
{/if}
|
||||
</Container>
|
||||
{#if error}
|
||||
<div class="text-red-300 text-center">{error}</div>
|
||||
{/if}
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
'text-primary-500': hasFocus
|
||||
})}
|
||||
>
|
||||
Interface
|
||||
General
|
||||
</span>
|
||||
</Container>
|
||||
<Container
|
||||
@@ -198,6 +198,14 @@
|
||||
localSettings.update((p) => ({ ...p, useCssTransitions: detail }))}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-lg font-medium text-secondary-100 py-2">
|
||||
<label class="mr-2">Check for Updates</label>
|
||||
<Toggle
|
||||
checked={$localSettings.checkForUpdates}
|
||||
on:change={({ detail }) =>
|
||||
localSettings.update((p) => ({ ...p, checkForUpdates: detail }))}
|
||||
/>
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
<Tab {...tab} tab={Tabs.Integrations} class="">
|
||||
|
||||
@@ -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}
|
||||
<Button type="primary-dark" action={handleConnectRadarr}>Connect</Button>
|
||||
{:else}
|
||||
<Button type="primary-dark" on:clickOrSelect={finalizeSetup}>Skip</Button>
|
||||
<Button type="primary-dark" on:clickOrSelect={() => tab.next()}>Skip</Button>
|
||||
{/if}
|
||||
</Container>
|
||||
</Tab>
|
||||
|
||||
<Tab {...tab} tab={Tabs.Complete} class={classNames(tabContainer, 'w-full')}>
|
||||
<div class="flex items-center justify-center text-secondary-500 mb-4">
|
||||
<CheckCircled size={64} />
|
||||
</div>
|
||||
<h1 class="header2 text-center w-full">All Set!</h1>
|
||||
<div class="header1 mb-8 text-center">Reiverr is now ready to use.</div>
|
||||
|
||||
<Container direction="horizontal" class="inline-flex space-x-4 w-full">
|
||||
<Button type="primary-dark" on:clickOrSelect={() => tab.previous()} icon={ArrowLeft}
|
||||
>Back</Button
|
||||
>
|
||||
<div class="flex-1">
|
||||
<Button type="primary-dark" on:clickOrSelect={finalizeSetup} iconAbsolute={ArrowRight}
|
||||
>Done</Button
|
||||
>
|
||||
</div>
|
||||
</Container>
|
||||
</Tab>
|
||||
</Container>
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user