feat: expose local auth endpoints

This commit is contained in:
thibaud-leclere 2026-05-06 08:31:39 +02:00
parent 0ec0ae442a
commit 60cf156230
2 changed files with 179 additions and 0 deletions

View file

@ -0,0 +1,144 @@
import { ConfigService } from '@nestjs/config';
import { mockDeep, mockReset } from 'jest-mock-extended';
import * as E from 'fp-ts/Either';
import { Response } from 'express';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { LocalAuthService } from './local-auth.service';
import { AuthUser } from 'src/types/AuthUser';
const mockAuthService = mockDeep<AuthService>();
const mockConfigService = mockDeep<ConfigService>();
const mockLocalAuthService = mockDeep<LocalAuthService>();
const authController = new AuthController(
mockAuthService,
mockConfigService,
mockLocalAuthService,
);
const currentTime = new Date();
const adminUser: AuthUser = {
uid: 'admin-1',
username: 'admin',
email: null,
displayName: 'Admin',
photoURL: null,
isAdmin: true,
refreshToken: null,
lastLoggedOn: currentTime,
lastActiveOn: currentTime,
createdOn: currentTime,
currentGQLSession: null,
currentRESTSession: null,
};
function createMockResponse() {
const res = {
cookie: jest.fn(),
status: jest.fn().mockReturnThis(),
send: jest.fn(),
};
return res as unknown as Response & typeof res;
}
describe('AuthController local auth', () => {
beforeEach(() => {
mockReset(mockAuthService);
mockReset(mockConfigService);
mockReset(mockLocalAuthService);
mockConfigService.get.mockImplementation((key: string) => {
if (key === 'INFRA.ACCESS_TOKEN_VALIDITY') return '86400000';
if (key === 'INFRA.REFRESH_TOKEN_VALIDITY') return '604800000';
if (key === 'INFRA.ALLOW_SECURE_COOKIES') return 'false';
return null;
});
});
it('sets auth cookies after local signin succeeds', async () => {
const res = createMockResponse();
mockLocalAuthService.signIn.mockResolvedValue(
E.right({
access_token: 'access-token',
refresh_token: 'refresh-token',
}),
);
await authController.signInLocal(
{
username: 'admin',
password: 'strong-password',
},
res,
);
expect(mockLocalAuthService.signIn).toHaveBeenCalledWith({
username: 'admin',
password: 'strong-password',
});
expect(res.cookie).toHaveBeenCalledTimes(2);
expect(res.status).toHaveBeenCalledWith(200);
});
it('sets auth cookies after local setup admin succeeds', async () => {
const res = createMockResponse();
mockLocalAuthService.setupFirstAdmin.mockResolvedValue(
E.right({
access_token: 'access-token',
refresh_token: 'refresh-token',
}),
);
await authController.setupLocalAdmin(
{
username: 'admin',
password: 'strong-password',
},
res,
);
expect(mockLocalAuthService.setupFirstAdmin).toHaveBeenCalledWith({
username: 'admin',
password: 'strong-password',
});
expect(res.cookie).toHaveBeenCalledTimes(2);
expect(res.status).toHaveBeenCalledWith(200);
});
it('delegates local user creation to the local auth service', async () => {
mockLocalAuthService.createLocalUser.mockResolvedValue(
E.right({
uid: 'user-1',
username: 'dwight',
displayName: 'Dwight Schrute',
email: null,
photoURL: null,
isAdmin: false,
}),
);
const result = await authController.createLocalUser(adminUser, {
username: 'dwight',
password: 'strong-password',
displayName: 'Dwight Schrute',
});
expect(mockLocalAuthService.createLocalUser).toHaveBeenCalledWith(
{
username: 'dwight',
password: 'strong-password',
displayName: 'Dwight Schrute',
},
adminUser,
);
expect(result).toEqual({
uid: 'user-1',
username: 'dwight',
displayName: 'Dwight Schrute',
email: null,
photoURL: null,
isAdmin: false,
});
});
});

View file

@ -30,6 +30,12 @@ import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
import { ConfigService } from '@nestjs/config';
import { throwHTTPErr } from 'src/utils';
import { UserLastLoginInterceptor } from 'src/interceptors/user-last-login.interceptor';
import {
CreateLocalUserDto,
LocalSetupAdminDto,
LocalSignInDto,
} from './dto/local-auth.dto';
import { LocalAuthService } from './local-auth.service';
@UseGuards(ThrottlerBehindProxyGuard)
@Controller({ path: 'auth', version: '1' })
@ -37,6 +43,7 @@ export class AuthController {
constructor(
private authService: AuthService,
private configService: ConfigService,
private localAuthService: LocalAuthService,
) {}
@Get('providers')
@ -80,6 +87,34 @@ export class AuthController {
authCookieHandler(res, authTokens.right, false, null, this.configService);
}
@Post('local/signin')
async signInLocal(@Body() data: LocalSignInDto, @Res() res: Response) {
const authTokens = await this.localAuthService.signIn(data);
if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left);
authCookieHandler(res, authTokens.right, false, null, this.configService);
}
@Post('local/setup-admin')
async setupLocalAdmin(
@Body() data: LocalSetupAdminDto,
@Res() res: Response,
) {
const authTokens = await this.localAuthService.setupFirstAdmin(data);
if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left);
authCookieHandler(res, authTokens.right, false, null, this.configService);
}
@Post('local/users')
@UseGuards(JwtAuthGuard)
async createLocalUser(
@GqlUser() user: AuthUser,
@Body() data: CreateLocalUserDto,
) {
const createdUser = await this.localAuthService.createLocalUser(data, user);
if (E.isLeft(createdUser)) throwHTTPErr(createdUser.left);
return createdUser.right;
}
/**
** Route to refresh auth tokens with Refresh Token Rotation
* @see https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation