feat: add local auth service
This commit is contained in:
parent
c8b7a172a4
commit
0ec0ae442a
5 changed files with 559 additions and 1 deletions
|
|
@ -16,6 +16,7 @@ import {
|
||||||
isInfraConfigTablePopulated,
|
isInfraConfigTablePopulated,
|
||||||
} from 'src/infra-config/helper';
|
} from 'src/infra-config/helper';
|
||||||
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
||||||
|
import { LocalAuthService } from './local-auth.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -29,7 +30,7 @@ import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
||||||
}),
|
}),
|
||||||
InfraConfigModule,
|
InfraConfigModule,
|
||||||
],
|
],
|
||||||
providers: [AuthService],
|
providers: [AuthService, LocalAuthService],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
})
|
})
|
||||||
export class AuthModule {
|
export class AuthModule {
|
||||||
|
|
|
||||||
29
packages/hoppscotch-backend/src/auth/dto/local-auth.dto.ts
Normal file
29
packages/hoppscotch-backend/src/auth/dto/local-auth.dto.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import {
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
Matches,
|
||||||
|
MaxLength,
|
||||||
|
MinLength,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export class LocalSignInDto {
|
||||||
|
@IsString()
|
||||||
|
@MinLength(3)
|
||||||
|
@MaxLength(40)
|
||||||
|
@Matches(/^[a-zA-Z0-9_.-]+$/)
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@MinLength(12)
|
||||||
|
@MaxLength(256)
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LocalSetupAdminDto extends LocalSignInDto {}
|
||||||
|
|
||||||
|
export class CreateLocalUserDto extends LocalSignInDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(80)
|
||||||
|
displayName?: string;
|
||||||
|
}
|
||||||
284
packages/hoppscotch-backend/src/auth/local-auth.service.spec.ts
Normal file
284
packages/hoppscotch-backend/src/auth/local-auth.service.spec.ts
Normal file
|
|
@ -0,0 +1,284 @@
|
||||||
|
import { HttpStatus } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||||
|
import * as argon2 from 'argon2';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
|
import {
|
||||||
|
AUTH_ADMIN_REQUIRED,
|
||||||
|
AUTH_INVALID_LOCAL_CREDENTIALS,
|
||||||
|
AUTH_LOCAL_SETUP_NOT_ALLOWED,
|
||||||
|
AUTH_USERNAME_ALREADY_EXISTS,
|
||||||
|
} from 'src/errors';
|
||||||
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
|
import { AuthTokens } from 'src/types/AuthTokens';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { LocalAuthService } from './local-auth.service';
|
||||||
|
|
||||||
|
jest.mock('argon2', () => ({
|
||||||
|
hash: jest.fn(),
|
||||||
|
verify: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
|
const mockAuthService = mockDeep<AuthService>();
|
||||||
|
const mockConfigService = mockDeep<ConfigService>();
|
||||||
|
|
||||||
|
const localAuthService = new LocalAuthService(
|
||||||
|
mockPrisma,
|
||||||
|
mockAuthService,
|
||||||
|
mockConfigService,
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentTime = new Date();
|
||||||
|
|
||||||
|
const user: AuthUser = {
|
||||||
|
uid: 'user-1',
|
||||||
|
username: 'dwight',
|
||||||
|
email: null,
|
||||||
|
displayName: 'Dwight Schrute',
|
||||||
|
photoURL: null,
|
||||||
|
isAdmin: false,
|
||||||
|
refreshToken: null,
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
|
lastActiveOn: currentTime,
|
||||||
|
createdOn: currentTime,
|
||||||
|
currentGQLSession: null,
|
||||||
|
currentRESTSession: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const adminUser: AuthUser = {
|
||||||
|
...user,
|
||||||
|
uid: 'admin-1',
|
||||||
|
username: 'admin',
|
||||||
|
isAdmin: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const tokens: AuthTokens = {
|
||||||
|
access_token: 'access-token',
|
||||||
|
refresh_token: 'refresh-token',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('LocalAuthService', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockReset(mockPrisma);
|
||||||
|
mockReset(mockAuthService);
|
||||||
|
mockReset(mockConfigService);
|
||||||
|
jest.mocked(argon2.hash).mockReset();
|
||||||
|
jest.mocked(argon2.verify).mockReset();
|
||||||
|
|
||||||
|
mockConfigService.get.mockReturnValue('LOCAL');
|
||||||
|
mockAuthService.generateAuthTokens.mockResolvedValue(E.right(tokens));
|
||||||
|
jest.mocked(argon2.hash).mockResolvedValue('hashed-password');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates the first local admin before onboarding is completed', async () => {
|
||||||
|
mockPrisma.infraConfig.findUnique.mockResolvedValue({
|
||||||
|
id: 'onboarding',
|
||||||
|
name: InfraConfigEnum.ONBOARDING_COMPLETED,
|
||||||
|
value: 'false',
|
||||||
|
lastSyncedEnvFileValue: null,
|
||||||
|
isEncrypted: false,
|
||||||
|
createdOn: currentTime,
|
||||||
|
updatedOn: currentTime,
|
||||||
|
});
|
||||||
|
mockPrisma.user.findFirst.mockResolvedValue(null);
|
||||||
|
mockPrisma.user.create.mockResolvedValue(adminUser);
|
||||||
|
|
||||||
|
const result = await localAuthService.setupFirstAdmin({
|
||||||
|
username: ' Dwight ',
|
||||||
|
password: 'strong-password',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockPrisma.user.create).toHaveBeenCalledWith({
|
||||||
|
data: {
|
||||||
|
username: 'dwight',
|
||||||
|
displayName: 'dwight',
|
||||||
|
isAdmin: true,
|
||||||
|
localCredential: {
|
||||||
|
create: {
|
||||||
|
passwordHash: 'hashed-password',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
providerAccounts: {
|
||||||
|
create: {
|
||||||
|
provider: 'LOCAL',
|
||||||
|
providerAccountId: 'dwight',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result).toEqualRight(tokens);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refuses setup admin after onboarding is completed', async () => {
|
||||||
|
mockPrisma.infraConfig.findUnique.mockResolvedValue({
|
||||||
|
id: 'onboarding',
|
||||||
|
name: InfraConfigEnum.ONBOARDING_COMPLETED,
|
||||||
|
value: 'true',
|
||||||
|
lastSyncedEnvFileValue: null,
|
||||||
|
isEncrypted: false,
|
||||||
|
createdOn: currentTime,
|
||||||
|
updatedOn: currentTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await localAuthService.setupFirstAdmin({
|
||||||
|
username: 'dwight',
|
||||||
|
password: 'strong-password',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqualLeft({
|
||||||
|
message: AUTH_LOCAL_SETUP_NOT_ALLOWED,
|
||||||
|
statusCode: HttpStatus.FORBIDDEN,
|
||||||
|
});
|
||||||
|
expect(mockPrisma.user.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('signs in a local user with valid credentials', async () => {
|
||||||
|
mockPrisma.user.findFirst.mockResolvedValue({
|
||||||
|
...user,
|
||||||
|
localCredential: {
|
||||||
|
id: 'credential-1',
|
||||||
|
userUid: user.uid,
|
||||||
|
passwordHash: 'hashed-password',
|
||||||
|
createdOn: currentTime,
|
||||||
|
updatedOn: currentTime,
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
jest.mocked(argon2.verify).mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await localAuthService.signIn({
|
||||||
|
username: 'Dwight',
|
||||||
|
password: 'strong-password',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockPrisma.user.findFirst).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
username: {
|
||||||
|
equals: 'dwight',
|
||||||
|
mode: 'insensitive',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
localCredential: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result).toEqualRight(tokens);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns generic invalid credentials for an unknown username', async () => {
|
||||||
|
mockPrisma.user.findFirst.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result = await localAuthService.signIn({
|
||||||
|
username: 'unknown',
|
||||||
|
password: 'strong-password',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqualLeft({
|
||||||
|
message: AUTH_INVALID_LOCAL_CREDENTIALS,
|
||||||
|
statusCode: HttpStatus.UNAUTHORIZED,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns generic invalid credentials for a bad password', async () => {
|
||||||
|
mockPrisma.user.findFirst.mockResolvedValue({
|
||||||
|
...user,
|
||||||
|
localCredential: {
|
||||||
|
id: 'credential-1',
|
||||||
|
userUid: user.uid,
|
||||||
|
passwordHash: 'hashed-password',
|
||||||
|
createdOn: currentTime,
|
||||||
|
updatedOn: currentTime,
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
jest.mocked(argon2.verify).mockResolvedValue(false);
|
||||||
|
|
||||||
|
const result = await localAuthService.signIn({
|
||||||
|
username: 'dwight',
|
||||||
|
password: 'wrong-password',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqualLeft({
|
||||||
|
message: AUTH_INVALID_LOCAL_CREDENTIALS,
|
||||||
|
statusCode: HttpStatus.UNAUTHORIZED,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refuses duplicate username case-insensitively', async () => {
|
||||||
|
mockPrisma.user.findFirst.mockResolvedValue(user);
|
||||||
|
|
||||||
|
const result = await localAuthService.createLocalUser(
|
||||||
|
{
|
||||||
|
username: 'Dwight',
|
||||||
|
password: 'strong-password',
|
||||||
|
displayName: 'Dwight Schrute',
|
||||||
|
},
|
||||||
|
adminUser,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqualLeft({
|
||||||
|
message: AUTH_USERNAME_ALREADY_EXISTS,
|
||||||
|
statusCode: HttpStatus.CONFLICT,
|
||||||
|
});
|
||||||
|
expect(mockPrisma.user.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates a non-admin local user when requested by an admin', async () => {
|
||||||
|
mockPrisma.user.findFirst.mockResolvedValue(null);
|
||||||
|
mockPrisma.user.create.mockResolvedValue(user);
|
||||||
|
|
||||||
|
const result = await localAuthService.createLocalUser(
|
||||||
|
{
|
||||||
|
username: 'Dwight',
|
||||||
|
password: 'strong-password',
|
||||||
|
displayName: 'Dwight Schrute',
|
||||||
|
},
|
||||||
|
adminUser,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockPrisma.user.create).toHaveBeenCalledWith({
|
||||||
|
data: {
|
||||||
|
username: 'dwight',
|
||||||
|
displayName: 'Dwight Schrute',
|
||||||
|
isAdmin: false,
|
||||||
|
localCredential: {
|
||||||
|
create: {
|
||||||
|
passwordHash: 'hashed-password',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
providerAccounts: {
|
||||||
|
create: {
|
||||||
|
provider: 'LOCAL',
|
||||||
|
providerAccountId: 'dwight',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result).toEqualRight({
|
||||||
|
uid: user.uid,
|
||||||
|
username: user.username,
|
||||||
|
displayName: user.displayName,
|
||||||
|
email: user.email,
|
||||||
|
photoURL: user.photoURL,
|
||||||
|
isAdmin: user.isAdmin,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refuses local user creation when requester is not admin', async () => {
|
||||||
|
const result = await localAuthService.createLocalUser(
|
||||||
|
{
|
||||||
|
username: 'jim',
|
||||||
|
password: 'strong-password',
|
||||||
|
displayName: 'Jim Halpert',
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqualLeft({
|
||||||
|
message: AUTH_ADMIN_REQUIRED,
|
||||||
|
statusCode: HttpStatus.FORBIDDEN,
|
||||||
|
});
|
||||||
|
expect(mockPrisma.user.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
210
packages/hoppscotch-backend/src/auth/local-auth.service.ts
Normal file
210
packages/hoppscotch-backend/src/auth/local-auth.service.ts
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
import { HttpStatus, Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import * as argon2 from 'argon2';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
|
import {
|
||||||
|
AUTH_ADMIN_REQUIRED,
|
||||||
|
AUTH_INVALID_LOCAL_CREDENTIALS,
|
||||||
|
AUTH_LOCAL_PROVIDER_DISABLED,
|
||||||
|
AUTH_LOCAL_SETUP_NOT_ALLOWED,
|
||||||
|
AUTH_USERNAME_ALREADY_EXISTS,
|
||||||
|
} from 'src/errors';
|
||||||
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
|
import { RESTError } from 'src/types/RESTError';
|
||||||
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
|
import { AuthProvider, authProviderCheck } from './helper';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import {
|
||||||
|
CreateLocalUserDto,
|
||||||
|
LocalSetupAdminDto,
|
||||||
|
LocalSignInDto,
|
||||||
|
} from './dto/local-auth.dto';
|
||||||
|
|
||||||
|
type LocalUserWithCredential = Awaited<
|
||||||
|
ReturnType<PrismaService['user']['findFirst']>
|
||||||
|
> & {
|
||||||
|
localCredential?: {
|
||||||
|
passwordHash: string;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LocalAuthService {
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly authService: AuthService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
normalizeUsername(username: string) {
|
||||||
|
return username.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private invalidCredentials() {
|
||||||
|
return E.left(<RESTError>{
|
||||||
|
message: AUTH_INVALID_LOCAL_CREDENTIALS,
|
||||||
|
statusCode: HttpStatus.UNAUTHORIZED,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureLocalProviderEnabled() {
|
||||||
|
const allowedProviders = this.configService.get<string>(
|
||||||
|
'INFRA.VITE_ALLOWED_AUTH_PROVIDERS',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!authProviderCheck(AuthProvider.LOCAL, allowedProviders)) {
|
||||||
|
return E.left(<RESTError>{
|
||||||
|
message: AUTH_LOCAL_PROVIDER_DISABLED,
|
||||||
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return E.right(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findUserByUsername(username: string) {
|
||||||
|
return this.prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
username: {
|
||||||
|
equals: username,
|
||||||
|
mode: 'insensitive',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
localCredential: true,
|
||||||
|
},
|
||||||
|
}) as Promise<LocalUserWithCredential | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureUsernameAvailable(username: string) {
|
||||||
|
const existingUser = await this.prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
username: {
|
||||||
|
equals: username,
|
||||||
|
mode: 'insensitive',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
return E.left(<RESTError>{
|
||||||
|
message: AUTH_USERNAME_ALREADY_EXISTS,
|
||||||
|
statusCode: HttpStatus.CONFLICT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return E.right(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createLocalUserRecord(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
displayName: string,
|
||||||
|
isAdmin: boolean,
|
||||||
|
) {
|
||||||
|
const passwordHash = await argon2.hash(password);
|
||||||
|
|
||||||
|
return this.prisma.user.create({
|
||||||
|
data: {
|
||||||
|
username,
|
||||||
|
displayName,
|
||||||
|
isAdmin,
|
||||||
|
localCredential: {
|
||||||
|
create: {
|
||||||
|
passwordHash,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
providerAccounts: {
|
||||||
|
create: {
|
||||||
|
provider: AuthProvider.LOCAL,
|
||||||
|
providerAccountId: username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setupFirstAdmin(dto: LocalSetupAdminDto) {
|
||||||
|
const onboardingCompleted = await this.prisma.infraConfig.findUnique({
|
||||||
|
where: {
|
||||||
|
name: InfraConfigEnum.ONBOARDING_COMPLETED,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onboardingCompleted?.value === 'true') {
|
||||||
|
return E.left(<RESTError>{
|
||||||
|
message: AUTH_LOCAL_SETUP_NOT_ALLOWED,
|
||||||
|
statusCode: HttpStatus.FORBIDDEN,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = this.normalizeUsername(dto.username);
|
||||||
|
const usernameAvailable = await this.ensureUsernameAvailable(username);
|
||||||
|
if (E.isLeft(usernameAvailable)) return usernameAvailable;
|
||||||
|
|
||||||
|
const user = await this.createLocalUserRecord(
|
||||||
|
username,
|
||||||
|
dto.password,
|
||||||
|
username,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.authService.generateAuthTokens(user.uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createLocalUser(dto: CreateLocalUserDto, requester: AuthUser) {
|
||||||
|
if (!requester?.isAdmin) {
|
||||||
|
return E.left(<RESTError>{
|
||||||
|
message: AUTH_ADMIN_REQUIRED,
|
||||||
|
statusCode: HttpStatus.FORBIDDEN,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerEnabled = await this.ensureLocalProviderEnabled();
|
||||||
|
if (E.isLeft(providerEnabled)) return providerEnabled;
|
||||||
|
|
||||||
|
const username = this.normalizeUsername(dto.username);
|
||||||
|
const usernameAvailable = await this.ensureUsernameAvailable(username);
|
||||||
|
if (E.isLeft(usernameAvailable)) return usernameAvailable;
|
||||||
|
|
||||||
|
const user = await this.createLocalUserRecord(
|
||||||
|
username,
|
||||||
|
dto.password,
|
||||||
|
dto.displayName || username,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
return E.right({
|
||||||
|
uid: user.uid,
|
||||||
|
username: user.username,
|
||||||
|
displayName: user.displayName,
|
||||||
|
email: user.email,
|
||||||
|
photoURL: user.photoURL,
|
||||||
|
isAdmin: user.isAdmin,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async signIn(dto: LocalSignInDto) {
|
||||||
|
const providerEnabled = await this.ensureLocalProviderEnabled();
|
||||||
|
if (E.isLeft(providerEnabled)) return providerEnabled;
|
||||||
|
|
||||||
|
const username = this.normalizeUsername(dto.username);
|
||||||
|
const user = await this.findUserByUsername(username);
|
||||||
|
|
||||||
|
if (!user?.localCredential?.passwordHash) {
|
||||||
|
return this.invalidCredentials();
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordMatches = await argon2.verify(
|
||||||
|
user.localCredential.passwordHash,
|
||||||
|
dto.password,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!passwordMatches) {
|
||||||
|
return this.invalidCredentials();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.authService.generateAuthTokens(user.uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -612,6 +612,40 @@ export const INVALID_ACCESS_TOKEN = 'auth/invalid_access_token' as const;
|
||||||
*/
|
*/
|
||||||
export const INVALID_REFRESH_TOKEN = 'auth/invalid_refresh_token' as const;
|
export const INVALID_REFRESH_TOKEN = 'auth/invalid_refresh_token' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local username/password authentication failed
|
||||||
|
* (LocalAuthService)
|
||||||
|
*/
|
||||||
|
export const AUTH_INVALID_LOCAL_CREDENTIALS =
|
||||||
|
'auth/invalid_local_credentials' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local auth provider is not enabled
|
||||||
|
* (LocalAuthService)
|
||||||
|
*/
|
||||||
|
export const AUTH_LOCAL_PROVIDER_DISABLED =
|
||||||
|
'auth/local_provider_disabled' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial local admin setup is no longer allowed
|
||||||
|
* (LocalAuthService)
|
||||||
|
*/
|
||||||
|
export const AUTH_LOCAL_SETUP_NOT_ALLOWED =
|
||||||
|
'auth/local_setup_not_allowed' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Username is already used by another account
|
||||||
|
* (LocalAuthService)
|
||||||
|
*/
|
||||||
|
export const AUTH_USERNAME_ALREADY_EXISTS =
|
||||||
|
'auth/username_already_exists' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin privileges are required for this auth action
|
||||||
|
* (LocalAuthService)
|
||||||
|
*/
|
||||||
|
export const AUTH_ADMIN_REQUIRED = 'auth/admin_required' as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The provided title for the user collection is short (less than 3 characters)
|
* The provided title for the user collection is short (less than 3 characters)
|
||||||
* (UserCollectionService)
|
* (UserCollectionService)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue