refactor(server): auth service (#1383)

* refactor: auth

* chore: tests

* Remove await on non-async method

* refactor: constants

* chore: remove extra async

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Jason Rasmussen
2023-01-23 23:13:42 -05:00
committed by GitHub
parent 443d08381a
commit eade36ee82
64 changed files with 1830 additions and 1901 deletions

View File

@@ -0,0 +1,7 @@
import { JwtModuleOptions } from '@nestjs/jwt';
import { jwtSecret } from './auth.constant';
export const jwtConfig: JwtModuleOptions = {
secret: jwtSecret,
signOptions: { expiresIn: '30d' },
};

View File

@@ -0,0 +1,7 @@
export const jwtSecret = process.env.JWT_SECRET;
export const IMMICH_ACCESS_COOKIE = 'immich_access_token';
export const IMMICH_AUTH_TYPE_COOKIE = 'immich_auth_type';
export enum AuthType {
PASSWORD = 'password',
OAUTH = 'oauth',
}

View File

@@ -0,0 +1,80 @@
import { SystemConfig, UserEntity } from '@app/infra/db/entities';
import { IncomingHttpHeaders } from 'http';
import { ISystemConfigRepository } from '../system-config';
import { SystemConfigCore } from '../system-config/system-config.core';
import { AuthType, IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE } from './auth.constant';
import { ICryptoRepository } from './crypto.repository';
import { JwtPayloadDto } from './dto/jwt-payload.dto';
import { LoginResponseDto, mapLoginResponse } from './response-dto';
export type JwtValidationResult = {
status: boolean;
userId: string | null;
};
export class AuthCore {
constructor(
private cryptoRepository: ICryptoRepository,
configRepository: ISystemConfigRepository,
private config: SystemConfig,
) {
const configCore = new SystemConfigCore(configRepository);
configCore.config$.subscribe((config) => (this.config = config));
}
isPasswordLoginEnabled() {
return this.config.passwordLogin.enabled;
}
public getCookies(loginResponse: LoginResponseDto, authType: AuthType, isSecure: boolean) {
const maxAge = 7 * 24 * 3600; // 7 days
let authTypeCookie = '';
let accessTokenCookie = '';
if (isSecure) {
accessTokenCookie = `${IMMICH_ACCESS_COOKIE}=${loginResponse.accessToken}; Secure; Path=/; Max-Age=${maxAge}; SameSite=Strict;`;
authTypeCookie = `${IMMICH_AUTH_TYPE_COOKIE}=${authType}; Secure; Path=/; Max-Age=${maxAge}; SameSite=Strict;`;
} else {
accessTokenCookie = `${IMMICH_ACCESS_COOKIE}=${loginResponse.accessToken}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Strict;`;
authTypeCookie = `${IMMICH_AUTH_TYPE_COOKIE}=${authType}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Strict;`;
}
return [accessTokenCookie, authTypeCookie];
}
public createLoginResponse(user: UserEntity, authType: AuthType, isSecure: boolean) {
const payload: JwtPayloadDto = { userId: user.id, email: user.email };
const accessToken = this.generateToken(payload);
const response = mapLoginResponse(user, accessToken);
const cookie = this.getCookies(response, authType, isSecure);
return { response, cookie };
}
validatePassword(inputPassword: string, user: UserEntity): boolean {
if (!user || !user.password) {
return false;
}
return this.cryptoRepository.compareSync(inputPassword, user.password);
}
extractJwtFromHeader(headers: IncomingHttpHeaders) {
if (!headers.authorization) {
return null;
}
const [type, accessToken] = headers.authorization.split(' ');
if (type.toLowerCase() !== 'bearer') {
return null;
}
return accessToken;
}
extractJwtFromCookie(cookies: Record<string, string>) {
return cookies?.[IMMICH_ACCESS_COOKIE] || null;
}
private generateToken(payload: JwtPayloadDto) {
return this.cryptoRepository.signJwt({ ...payload });
}
}

View File

@@ -0,0 +1,263 @@
import { SystemConfig, UserEntity } from '@app/infra/db/entities';
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import { generators, Issuer } from 'openid-client';
import { Socket } from 'socket.io';
import {
authStub,
entityStub,
loginResponseStub,
newCryptoRepositoryMock,
newSystemConfigRepositoryMock,
newUserRepositoryMock,
systemConfigStub,
} from '../../test';
import { ISystemConfigRepository } from '../system-config';
import { IUserRepository } from '../user';
import { AuthType, IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE } from './auth.constant';
import { AuthService } from './auth.service';
import { ICryptoRepository } from './crypto.repository';
import { SignUpDto } from './dto';
const email = 'test@immich.com';
const sub = 'my-auth-user-sub';
const fixtures = {
login: {
email,
password: 'password',
},
};
const CLIENT_IP = '127.0.0.1';
jest.mock('@nestjs/common', () => ({
...jest.requireActual('@nestjs/common'),
Logger: jest.fn().mockReturnValue({
verbose: jest.fn(),
debug: jest.fn(),
log: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
}),
}));
describe('AuthService', () => {
let sut: AuthService;
let cryptoMock: jest.Mocked<ICryptoRepository>;
let userMock: jest.Mocked<IUserRepository>;
let configMock: jest.Mocked<ISystemConfigRepository>;
let callbackMock: jest.Mock;
let create: (config: SystemConfig) => AuthService;
afterEach(() => {
jest.resetModules();
});
beforeEach(async () => {
callbackMock = jest.fn().mockReturnValue({ access_token: 'access-token' });
jest.spyOn(generators, 'state').mockReturnValue('state');
jest.spyOn(Issuer, 'discover').mockResolvedValue({
id_token_signing_alg_values_supported: ['HS256'],
Client: jest.fn().mockResolvedValue({
issuer: {
metadata: {
end_session_endpoint: 'http://end-session-endpoint',
},
},
authorizationUrl: jest.fn().mockReturnValue('http://authorization-url'),
callbackParams: jest.fn().mockReturnValue({ state: 'state' }),
callback: callbackMock,
userinfo: jest.fn().mockResolvedValue({ sub, email }),
}),
} as any);
cryptoMock = newCryptoRepositoryMock();
userMock = newUserRepositoryMock();
configMock = newSystemConfigRepositoryMock();
create = (config) => new AuthService(cryptoMock, configMock, userMock, config);
sut = create(systemConfigStub.enabled);
});
it('should be defined', () => {
expect(sut).toBeDefined();
});
describe('login', () => {
it('should throw an error if password login is disabled', async () => {
sut = create(systemConfigStub.disabled);
await expect(sut.login(fixtures.login, CLIENT_IP, true)).rejects.toBeInstanceOf(UnauthorizedException);
});
it('should check the user exists', async () => {
userMock.getByEmail.mockResolvedValue(null);
await expect(sut.login(fixtures.login, CLIENT_IP, true)).rejects.toBeInstanceOf(BadRequestException);
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
});
it('should check the user has a password', async () => {
userMock.getByEmail.mockResolvedValue({} as UserEntity);
await expect(sut.login(fixtures.login, CLIENT_IP, true)).rejects.toBeInstanceOf(BadRequestException);
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
});
it('should successfully log the user in', async () => {
userMock.getByEmail.mockResolvedValue(entityStub.user1);
await expect(sut.login(fixtures.login, CLIENT_IP, true)).resolves.toEqual(loginResponseStub.user1password);
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
});
it('should generate the cookie headers (insecure)', async () => {
userMock.getByEmail.mockResolvedValue(entityStub.user1);
await expect(sut.login(fixtures.login, CLIENT_IP, false)).resolves.toEqual(loginResponseStub.user1insecure);
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
});
});
describe('changePassword', () => {
it('should change the password', async () => {
const authUser = { email: 'test@imimch.com' } as UserEntity;
const dto = { password: 'old-password', newPassword: 'new-password' };
userMock.getByEmail.mockResolvedValue({
email: 'test@immich.com',
password: 'hash-password',
} as UserEntity);
await sut.changePassword(authUser, dto);
expect(userMock.getByEmail).toHaveBeenCalledWith(authUser.email, true);
expect(cryptoMock.compareSync).toHaveBeenCalledWith('old-password', 'hash-password');
});
it('should throw when auth user email is not found', async () => {
const authUser = { email: 'test@imimch.com' } as UserEntity;
const dto = { password: 'old-password', newPassword: 'new-password' };
userMock.getByEmail.mockResolvedValue(null);
await expect(sut.changePassword(authUser, dto)).rejects.toBeInstanceOf(UnauthorizedException);
});
it('should throw when password does not match existing password', async () => {
const authUser = { email: 'test@imimch.com' } as UserEntity;
const dto = { password: 'old-password', newPassword: 'new-password' };
cryptoMock.compareSync.mockReturnValue(false);
userMock.getByEmail.mockResolvedValue({
email: 'test@immich.com',
password: 'hash-password',
} as UserEntity);
await expect(sut.changePassword(authUser, dto)).rejects.toBeInstanceOf(BadRequestException);
});
it('should throw when user does not have a password', async () => {
const authUser = { email: 'test@imimch.com' } as UserEntity;
const dto = { password: 'old-password', newPassword: 'new-password' };
cryptoMock.compareSync.mockReturnValue(false);
userMock.getByEmail.mockResolvedValue({
email: 'test@immich.com',
password: '',
} as UserEntity);
await expect(sut.changePassword(authUser, dto)).rejects.toBeInstanceOf(BadRequestException);
});
});
describe('logout', () => {
it('should return the end session endpoint', async () => {
await expect(sut.logout(AuthType.OAUTH)).resolves.toEqual({
successful: true,
redirectUri: 'http://end-session-endpoint',
});
});
it('should return the default redirect', async () => {
await expect(sut.logout(AuthType.PASSWORD)).resolves.toEqual({
successful: true,
redirectUri: '/auth/login?autoLaunch=0',
});
});
});
describe('adminSignUp', () => {
const dto: SignUpDto = { email: 'test@immich.com', password: 'password', firstName: 'immich', lastName: 'admin' };
it('should only allow one admin', async () => {
userMock.getAdmin.mockResolvedValue({} as UserEntity);
await expect(sut.adminSignUp(dto)).rejects.toBeInstanceOf(BadRequestException);
expect(userMock.getAdmin).toHaveBeenCalled();
});
it('should sign up the admin', async () => {
userMock.getAdmin.mockResolvedValue(null);
userMock.create.mockResolvedValue({ ...dto, id: 'admin', createdAt: 'today' } as UserEntity);
await expect(sut.adminSignUp(dto)).resolves.toEqual({
id: 'admin',
createdAt: 'today',
email: 'test@immich.com',
firstName: 'immich',
lastName: 'admin',
});
expect(userMock.getAdmin).toHaveBeenCalled();
expect(userMock.create).toHaveBeenCalled();
});
});
describe('validateSocket', () => {
it('should validate using authorization header', async () => {
userMock.get.mockResolvedValue(entityStub.user1);
const client = { handshake: { headers: { authorization: 'Bearer jwt-token' } } };
await expect(sut.validateSocket(client as Socket)).resolves.toEqual(entityStub.user1);
});
});
describe('validatePayload', () => {
it('should throw if no user is found', async () => {
userMock.get.mockResolvedValue(null);
await expect(sut.validatePayload({ email: 'a', userId: 'test' })).rejects.toBeInstanceOf(UnauthorizedException);
});
it('should return an auth dto', async () => {
userMock.get.mockResolvedValue(entityStub.admin);
await expect(sut.validatePayload({ email: 'a', userId: 'test' })).resolves.toEqual(authStub.admin);
});
});
describe('extractJwtFromCookie', () => {
it('should extract the access token', () => {
const cookie = { [IMMICH_ACCESS_COOKIE]: 'signed-jwt', [IMMICH_AUTH_TYPE_COOKIE]: 'password' };
expect(sut.extractJwtFromCookie(cookie)).toEqual('signed-jwt');
});
it('should work with no cookies', () => {
expect(sut.extractJwtFromCookie(undefined as any)).toBeNull();
});
it('should work on empty cookies', () => {
expect(sut.extractJwtFromCookie({})).toBeNull();
});
});
describe('extractJwtFromHeader', () => {
it('should extract the access token', () => {
expect(sut.extractJwtFromHeader({ authorization: `Bearer signed-jwt` })).toEqual('signed-jwt');
});
it('should work without the auth header', () => {
expect(sut.extractJwtFromHeader({})).toBeNull();
});
it('should ignore basic auth', () => {
expect(sut.extractJwtFromHeader({ authorization: `Basic stuff` })).toBeNull();
});
});
});

View File

@@ -0,0 +1,160 @@
import { SystemConfig } from '@app/infra/db/entities';
import {
BadRequestException,
Inject,
Injectable,
InternalServerErrorException,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import * as cookieParser from 'cookie';
import { IncomingHttpHeaders } from 'http';
import { Socket } from 'socket.io';
import { OAuthCore } from '../oauth/oauth.core';
import { INITIAL_SYSTEM_CONFIG, ISystemConfigRepository } from '../system-config';
import { IUserRepository, UserCore, UserResponseDto } from '../user';
import { AuthType, jwtSecret } from './auth.constant';
import { AuthCore } from './auth.core';
import { ICryptoRepository } from './crypto.repository';
import { AuthUserDto, ChangePasswordDto, JwtPayloadDto, LoginCredentialDto, SignUpDto } from './dto';
import { AdminSignupResponseDto, LoginResponseDto, LogoutResponseDto, mapAdminSignupResponse } from './response-dto';
@Injectable()
export class AuthService {
private authCore: AuthCore;
private oauthCore: OAuthCore;
private userCore: UserCore;
private logger = new Logger(AuthService.name);
constructor(
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
@Inject(IUserRepository) userRepository: IUserRepository,
@Inject(INITIAL_SYSTEM_CONFIG) initialConfig: SystemConfig,
) {
this.authCore = new AuthCore(cryptoRepository, configRepository, initialConfig);
this.oauthCore = new OAuthCore(configRepository, initialConfig);
this.userCore = new UserCore(userRepository);
}
public async login(
loginCredential: LoginCredentialDto,
clientIp: string,
isSecure: boolean,
): Promise<{ response: LoginResponseDto; cookie: string[] }> {
if (!this.authCore.isPasswordLoginEnabled()) {
throw new UnauthorizedException('Password login has been disabled');
}
let user = await this.userCore.getByEmail(loginCredential.email, true);
if (user) {
const isAuthenticated = await this.authCore.validatePassword(loginCredential.password, user);
if (!isAuthenticated) {
user = null;
}
}
if (!user) {
this.logger.warn(`Failed login attempt for user ${loginCredential.email} from ip address ${clientIp}`);
throw new BadRequestException('Incorrect email or password');
}
return this.authCore.createLoginResponse(user, AuthType.PASSWORD, isSecure);
}
public async logout(authType: AuthType): Promise<LogoutResponseDto> {
if (authType === AuthType.OAUTH) {
const url = await this.oauthCore.getLogoutEndpoint();
if (url) {
return { successful: true, redirectUri: url };
}
}
return { successful: true, redirectUri: '/auth/login?autoLaunch=0' };
}
public async changePassword(authUser: AuthUserDto, dto: ChangePasswordDto) {
const { password, newPassword } = dto;
const user = await this.userCore.getByEmail(authUser.email, true);
if (!user) {
throw new UnauthorizedException();
}
const valid = await this.authCore.validatePassword(password, user);
if (!valid) {
throw new BadRequestException('Wrong password');
}
return this.userCore.updateUser(authUser, authUser.id, { password: newPassword });
}
public async adminSignUp(dto: SignUpDto): Promise<AdminSignupResponseDto> {
const adminUser = await this.userCore.getAdmin();
if (adminUser) {
throw new BadRequestException('The server already has an admin');
}
try {
const admin = await this.userCore.createUser({
isAdmin: true,
email: dto.email,
firstName: dto.firstName,
lastName: dto.lastName,
password: dto.password,
});
return mapAdminSignupResponse(admin);
} catch (error) {
this.logger.error(`Unable to register admin user: ${error}`, (error as Error).stack);
throw new InternalServerErrorException('Failed to register new admin user');
}
}
async validateSocket(client: Socket): Promise<UserResponseDto | null> {
try {
const headers = client.handshake.headers;
const accessToken =
this.extractJwtFromCookie(cookieParser.parse(headers.cookie || '')) || this.extractJwtFromHeader(headers);
if (accessToken) {
const payload = await this.cryptoRepository.verifyJwtAsync<JwtPayloadDto>(accessToken, { secret: jwtSecret });
if (payload?.userId && payload?.email) {
const user = await this.userCore.get(payload.userId);
if (user) {
return user;
}
}
}
} catch (e) {
return null;
}
return null;
}
async validatePayload(payload: JwtPayloadDto) {
const { userId } = payload;
const user = await this.userCore.get(userId);
if (!user) {
throw new UnauthorizedException('Failure to validate JWT payload');
}
const authUser = new AuthUserDto();
authUser.id = user.id;
authUser.email = user.email;
authUser.isAdmin = user.isAdmin;
authUser.isPublicUser = false;
authUser.isAllowUpload = true;
return authUser;
}
extractJwtFromCookie(cookies: Record<string, string>) {
return this.authCore.extractJwtFromCookie(cookies);
}
extractJwtFromHeader(headers: IncomingHttpHeaders) {
return this.authCore.extractJwtFromHeader(headers);
}
}

View File

@@ -1,7 +1,11 @@
import { JwtSignOptions, JwtVerifyOptions } from '@nestjs/jwt';
export const ICryptoRepository = 'ICryptoRepository';
export interface ICryptoRepository {
randomBytes(size: number): Buffer;
hash(data: string | Buffer, saltOrRounds: string | number): Promise<string>;
compareSync(data: Buffer | string, encrypted: string): boolean;
signJwt(payload: string | Buffer | object, options?: JwtSignOptions): string;
verifyJwtAsync<T extends object = any>(token: string, options?: JwtVerifyOptions): Promise<T>;
}

View File

@@ -0,0 +1,15 @@
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 +1,5 @@
export * from './auth-user.dto';
export * from './change-password.dto';
export * from './jwt-payload.dto';
export * from './login-credential.dto';
export * from './sign-up.dto';

View File

@@ -0,0 +1,4 @@
export class JwtPayloadDto {
userId!: string;
email!: string;
}

View File

@@ -0,0 +1,33 @@
import { plainToInstance } from 'class-transformer';
import { validateSync } from 'class-validator';
import { LoginCredentialDto } from './login-credential.dto';
describe('LoginCredentialDto', () => {
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

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

View File

@@ -0,0 +1,44 @@
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 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

@@ -0,0 +1,25 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
export class SignUpDto {
@IsEmail()
@ApiProperty({ example: 'testuser@email.com' })
@Transform(({ value }) => value.toLowerCase())
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,2 +1,6 @@
export * from './auth.config';
export * from './auth.constant';
export * from './auth.service';
export * from './crypto.repository';
export * from './dto';
export * from './response-dto';

View File

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

View File

@@ -0,0 +1,4 @@
export * from './admin-signup-response.dto';
export * from './login-response.dto';
export * from './logout-response.dto';
export * from './validate-asset-token-response.dto';

View File

@@ -0,0 +1,41 @@
import { UserEntity } from '@app/infra/db/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

@@ -0,0 +1,13 @@
import { ApiResponseProperty } from '@nestjs/swagger';
export class LogoutResponseDto {
constructor(successful: boolean) {
this.successful = successful;
}
@ApiResponseProperty()
successful!: boolean;
@ApiResponseProperty()
redirectUri!: string;
}

View File

@@ -0,0 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
export class ValidateAccessTokenResponseDto {
constructor(authStatus: boolean) {
this.authStatus = authStatus;
}
@ApiProperty({ type: 'boolean' })
authStatus!: boolean;
}