refactor(server): auth dtos (#4881)

* refactor: auth dtos

* chore: open api
This commit is contained in:
Jason Rasmussen
2023-11-09 10:14:15 -05:00
committed by GitHub
parent 5c602bf4d4
commit 5423f1c25b
38 changed files with 187 additions and 657 deletions

View File

@@ -0,0 +1,132 @@
import { UserEntity, UserTokenEntity } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
export class AuthUserDto {
id!: string;
email!: string;
isAdmin!: boolean;
isPublicUser?: boolean;
sharedLinkId?: string;
isAllowUpload?: boolean;
isAllowDownload?: boolean;
isShowMetadata?: boolean;
accessTokenId?: string;
externalPath?: string | null;
}
export class LoginCredentialDto {
@IsEmail({ require_tld: false })
@Transform(({ value }) => value?.toLowerCase())
@IsNotEmpty()
@ApiProperty({ example: 'testuser@email.com' })
email!: string;
@IsString()
@IsNotEmpty()
@ApiProperty({ example: 'password' })
password!: string;
}
export class LoginResponseDto {
accessToken!: string;
userId!: string;
userEmail!: string;
firstName!: string;
lastName!: string;
profileImagePath!: string;
isAdmin!: boolean;
shouldChangePassword!: boolean;
}
export function mapLoginResponse(entity: UserEntity, accessToken: string): LoginResponseDto {
return {
accessToken: accessToken,
userId: entity.id,
userEmail: entity.email,
firstName: entity.firstName,
lastName: entity.lastName,
isAdmin: entity.isAdmin,
profileImagePath: entity.profileImagePath,
shouldChangePassword: entity.shouldChangePassword,
};
}
export class LogoutResponseDto {
successful!: boolean;
redirectUri!: string;
}
export class SignUpDto extends LoginCredentialDto {
@IsString()
@IsNotEmpty()
@ApiProperty({ example: 'Admin' })
firstName!: string;
@IsString()
@IsNotEmpty()
@ApiProperty({ example: 'Doe' })
lastName!: string;
}
export class ChangePasswordDto {
@IsString()
@IsNotEmpty()
@ApiProperty({ example: 'password' })
password!: string;
@IsString()
@IsNotEmpty()
@MinLength(8)
@ApiProperty({ example: 'password' })
newPassword!: string;
}
export class ValidateAccessTokenResponseDto {
authStatus!: boolean;
}
export class AuthDeviceResponseDto {
id!: string;
createdAt!: string;
updatedAt!: string;
current!: boolean;
deviceType!: string;
deviceOS!: string;
}
export const mapUserToken = (entity: UserTokenEntity, currentId?: string): AuthDeviceResponseDto => ({
id: entity.id,
createdAt: entity.createdAt.toISOString(),
updatedAt: entity.updatedAt.toISOString(),
current: currentId === entity.id,
deviceOS: entity.deviceOS,
deviceType: entity.deviceType,
});
export class OAuthCallbackDto {
@IsNotEmpty()
@IsString()
@ApiProperty()
url!: string;
}
export class OAuthConfigDto {
@IsNotEmpty()
@IsString()
redirectUri!: string;
}
/** @deprecated use oauth authorize */
export class OAuthConfigResponseDto {
enabled!: boolean;
passwordLoginEnabled!: boolean;
url?: string;
buttonText?: string;
autoLaunch?: boolean;
}
export class OAuthAuthorizeResponseDto {
url!: string;
}

View File

@@ -31,8 +31,8 @@ import {
IUserTokenRepository,
} from '../repositories';
import { AuthType } from './auth.constant';
import { AuthUserDto, SignUpDto } from './auth.dto';
import { AuthService } from './auth.service';
import { AuthUserDto, SignUpDto } from './dto';
// const token = Buffer.from('my-api-key', 'utf8').toString('base64');

View File

@@ -32,18 +32,21 @@ import {
LOGIN_URL,
MOBILE_REDIRECT,
} from './auth.constant';
import { AuthUserDto, ChangePasswordDto, LoginCredentialDto, OAuthCallbackDto, OAuthConfigDto, SignUpDto } from './dto';
import {
AdminSignupResponseDto,
AuthDeviceResponseDto,
AuthUserDto,
ChangePasswordDto,
LoginCredentialDto,
LoginResponseDto,
LogoutResponseDto,
OAuthAuthorizeResponseDto,
OAuthCallbackDto,
OAuthConfigDto,
OAuthConfigResponseDto,
mapAdminSignupResponse,
SignUpDto,
mapLoginResponse,
mapUserToken,
} from './response-dto';
} from './auth.dto';
export interface LoginDetails {
isSecure: boolean;
@@ -133,7 +136,7 @@ export class AuthService {
return this.userCore.updateUser(authUser, authUser.id, { password: newPassword });
}
async adminSignUp(dto: SignUpDto): Promise<AdminSignupResponseDto> {
async adminSignUp(dto: SignUpDto): Promise<UserResponseDto> {
const adminUser = await this.userRepository.getAdmin();
if (adminUser) {
@@ -149,7 +152,7 @@ export class AuthService {
storageLabel: 'admin',
});
return mapAdminSignupResponse(admin);
return mapUser(admin);
}
async validate(headers: IncomingHttpHeaders, params: Record<string, string>): Promise<AuthUserDto> {

View File

@@ -1,12 +0,0 @@
export class AuthUserDto {
id!: string;
email!: string;
isAdmin!: boolean;
isPublicUser?: boolean;
sharedLinkId?: string;
isAllowUpload?: boolean;
isAllowDownload?: boolean;
isShowMetadata?: boolean;
accessTokenId?: string;
externalPath?: string | null;
}

View File

@@ -1,15 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, MinLength } from 'class-validator';
export class ChangePasswordDto {
@IsString()
@IsNotEmpty()
@ApiProperty({ example: 'password' })
password!: string;
@IsString()
@IsNotEmpty()
@MinLength(8)
@ApiProperty({ example: 'password' })
newPassword!: string;
}

View File

@@ -1,6 +0,0 @@
export * from './auth-user.dto';
export * from './change-password.dto';
export * from './login-credential.dto';
export * from './oauth-auth-code.dto';
export * from './oauth-config.dto';
export * from './sign-up.dto';

View File

@@ -1,42 +0,0 @@
import { plainToInstance } from 'class-transformer';
import { validateSync } from 'class-validator';
import { LoginCredentialDto } from './login-credential.dto';
describe('LoginCredentialDto', () => {
it('should allow emails without a tld', () => {
const someEmail = 'test@test';
const dto = plainToInstance(LoginCredentialDto, { email: someEmail, password: 'password' });
const errors = validateSync(dto);
expect(errors).toHaveLength(0);
expect(dto.email).toEqual(someEmail);
});
it('should fail without an email', () => {
const dto = plainToInstance(LoginCredentialDto, { password: 'password' });
const errors = validateSync(dto);
expect(errors).toHaveLength(1);
expect(errors[0].property).toEqual('email');
});
it('should fail with an invalid email', () => {
const dto = plainToInstance(LoginCredentialDto, { email: 'invalid.com', password: 'password' });
const errors = validateSync(dto);
expect(errors).toHaveLength(1);
expect(errors[0].property).toEqual('email');
});
it('should make the email all lowercase', () => {
const dto = plainToInstance(LoginCredentialDto, { email: 'TeSt@ImMiCh.com', password: 'password' });
const errors = validateSync(dto);
expect(errors).toHaveLength(0);
expect(dto.email).toEqual('test@immich.com');
});
it('should fail without a password', () => {
const dto = plainToInstance(LoginCredentialDto, { email: 'test@immich.com', password: '' });
const errors = validateSync(dto);
expect(errors).toHaveLength(1);
expect(errors[0].property).toEqual('password');
});
});

View File

@@ -1,16 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
export class LoginCredentialDto {
@IsEmail({ require_tld: false })
@Transform(({ value }) => value?.toLowerCase())
@IsNotEmpty()
@ApiProperty({ example: 'testuser@email.com' })
email!: string;
@IsString()
@IsNotEmpty()
@ApiProperty({ example: 'password' })
password!: string;
}

View File

@@ -1,9 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class OAuthCallbackDto {
@IsNotEmpty()
@IsString()
@ApiProperty()
url!: string;
}

View File

@@ -1,7 +0,0 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class OAuthConfigDto {
@IsNotEmpty()
@IsString()
redirectUri!: string;
}

View File

@@ -1,58 +0,0 @@
import { plainToInstance } from 'class-transformer';
import { validateSync } from 'class-validator';
import { SignUpDto } from './sign-up.dto';
describe('SignUpDto', () => {
it('should require all fields', () => {
const dto = plainToInstance(SignUpDto, {
email: '',
password: '',
firstName: '',
lastName: '',
});
const errors = validateSync(dto);
expect(errors).toHaveLength(4);
expect(errors[0].property).toEqual('email');
expect(errors[1].property).toEqual('password');
expect(errors[2].property).toEqual('firstName');
expect(errors[3].property).toEqual('lastName');
});
it('should require a valid email', () => {
const dto = plainToInstance(SignUpDto, {
email: 'immich.com',
password: 'password',
firstName: 'first name',
lastName: 'last name',
});
const errors = validateSync(dto);
expect(errors).toHaveLength(1);
expect(errors[0].property).toEqual('email');
});
it('should allow emails without a tld', () => {
const someEmail = 'test@test';
const dto = plainToInstance(SignUpDto, {
email: someEmail,
password: 'password',
firstName: 'first name',
lastName: 'last name',
});
const errors = validateSync(dto);
expect(errors).toHaveLength(0);
expect(dto.email).toEqual(someEmail);
});
it('should make the email all lowercase', () => {
const dto = plainToInstance(SignUpDto, {
email: 'TeSt@ImMiCh.com',
password: 'password',
firstName: 'first name',
lastName: 'last name',
});
const errors = validateSync(dto);
expect(errors).toHaveLength(0);
expect(dto.email).toEqual('test@immich.com');
});
});

View File

@@ -1,26 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
export class SignUpDto {
@IsEmail({ require_tld: false })
@Transform(({ value }) => value?.toLowerCase())
@IsNotEmpty()
@ApiProperty({ example: 'testuser@email.com' })
email!: string;
@IsString()
@IsNotEmpty()
@ApiProperty({ example: 'password' })
password!: string;
@IsString()
@IsNotEmpty()
@ApiProperty({ example: 'Admin' })
firstName!: string;
@IsString()
@IsNotEmpty()
@ApiProperty({ example: 'Doe' })
lastName!: string;
}

View File

@@ -1,4 +1,3 @@
export * from './auth.constant';
export * from './auth.dto';
export * from './auth.service';
export * from './dto';
export * from './response-dto';

View File

@@ -1,19 +0,0 @@
import { UserEntity } from '@app/infra/entities';
export class AdminSignupResponseDto {
id!: string;
email!: string;
firstName!: string;
lastName!: string;
createdAt!: Date;
}
export function mapAdminSignupResponse(entity: UserEntity): AdminSignupResponseDto {
return {
id: entity.id,
email: entity.email,
firstName: entity.firstName,
lastName: entity.lastName,
createdAt: entity.createdAt,
};
}

View File

@@ -1,19 +0,0 @@
import { UserTokenEntity } from '@app/infra/entities';
export class AuthDeviceResponseDto {
id!: string;
createdAt!: string;
updatedAt!: string;
current!: boolean;
deviceType!: string;
deviceOS!: string;
}
export const mapUserToken = (entity: UserTokenEntity, currentId?: string): AuthDeviceResponseDto => ({
id: entity.id,
createdAt: entity.createdAt.toISOString(),
updatedAt: entity.updatedAt.toISOString(),
current: currentId === entity.id,
deviceOS: entity.deviceOS,
deviceType: entity.deviceType,
});

View File

@@ -1,6 +0,0 @@
export * from './admin-signup-response.dto';
export * from './auth-device-response.dto';
export * from './login-response.dto';
export * from './logout-response.dto';
export * from './oauth-config-response.dto';
export * from './validate-asset-token-response.dto';

View File

@@ -1,41 +0,0 @@
import { UserEntity } from '@app/infra/entities';
import { ApiResponseProperty } from '@nestjs/swagger';
export class LoginResponseDto {
@ApiResponseProperty()
accessToken!: string;
@ApiResponseProperty()
userId!: string;
@ApiResponseProperty()
userEmail!: string;
@ApiResponseProperty()
firstName!: string;
@ApiResponseProperty()
lastName!: string;
@ApiResponseProperty()
profileImagePath!: string;
@ApiResponseProperty()
isAdmin!: boolean;
@ApiResponseProperty()
shouldChangePassword!: boolean;
}
export function mapLoginResponse(entity: UserEntity, accessToken: string): LoginResponseDto {
return {
accessToken: accessToken,
userId: entity.id,
userEmail: entity.email,
firstName: entity.firstName,
lastName: entity.lastName,
isAdmin: entity.isAdmin,
profileImagePath: entity.profileImagePath,
shouldChangePassword: entity.shouldChangePassword,
};
}

View File

@@ -1,4 +0,0 @@
export class LogoutResponseDto {
successful!: boolean;
redirectUri!: string;
}

View File

@@ -1,11 +0,0 @@
export class OAuthConfigResponseDto {
enabled!: boolean;
passwordLoginEnabled!: boolean;
url?: string;
buttonText?: string;
autoLaunch?: boolean;
}
export class OAuthAuthorizeResponseDto {
url!: string;
}

View File

@@ -1,3 +0,0 @@
export class ValidateAccessTokenResponseDto {
authStatus!: boolean;
}

View File

@@ -1,5 +1,4 @@
import {
AdminSignupResponseDto,
AuthDeviceResponseDto,
AuthService,
AuthUserDto,
@@ -15,7 +14,7 @@ import {
ValidateAccessTokenResponseDto,
} from '@app/domain';
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Req, Res } from '@nestjs/common';
import { ApiBadRequestResponse, ApiTags } from '@nestjs/swagger';
import { ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express';
import { AuthUser, Authenticated, GetLoginDetails, PublicRoute } from '../app.guard';
import { UseValidation } from '../app.utils';
@@ -42,9 +41,8 @@ export class AuthController {
@PublicRoute()
@Post('admin-sign-up')
@ApiBadRequestResponse({ description: 'The server already has an admin' })
signUpAdmin(@Body() signUpCredential: SignUpDto): Promise<AdminSignupResponseDto> {
return this.service.adminSignUp(signUpCredential);
signUpAdmin(@Body() dto: SignUpDto): Promise<UserResponseDto> {
return this.service.adminSignUp(dto);
}
@Get('devices')