Skip to content

Commit

Permalink
feat: progressive auth service (#71)
Browse files Browse the repository at this point in the history
  • Loading branch information
okjodom authored Jan 16, 2025
1 parent 06cb4b8 commit 4b845e0
Show file tree
Hide file tree
Showing 22 changed files with 298 additions and 74 deletions.
1 change: 1 addition & 0 deletions apps/api/.dev.env
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
PORT=4000
NODE_ENV='development'
AUTH_GRPC_URL='auth:4010'
SWAP_GRPC_URL='swap:4040'
NOSTR_GRPC_URL='nostr:4050'
SMS_GRPC_URL='sms:4060'
Expand Down
25 changes: 22 additions & 3 deletions apps/api/src/api.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { join } from 'path';
import * as Joi from 'joi';
import { join } from 'path';
import { JwtModule } from '@nestjs/jwt';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ClientsModule, Transport } from '@nestjs/microservices';
import {
AUTH_SERVICE_NAME,
EVENTS_SERVICE_BUS,
LoggerModule,
NOSTR_SERVICE_NAME,
Expand All @@ -22,9 +24,12 @@ import { AdminController } from './admin/admin.controller';
import { AdminService } from './admin/admin.service';
import { SolowalletService } from './solowallet/solowallet.service';
import { SolowalletController } from './solowallet/solowallet.controller';
import { AuthService } from './auth/auth.service';
import { AuthController } from './auth/auth.controller';

@Module({
imports: [
JwtModule,
LoggerModule,
ConfigModule.forRoot({
isGlobal: true,
Expand All @@ -41,6 +46,18 @@ import { SolowalletController } from './solowallet/solowallet.controller';
}),
}),
ClientsModule.registerAsync([
{
name: AUTH_SERVICE_NAME,
useFactory: (configService: ConfigService) => ({
transport: Transport.GRPC,
options: {
package: 'auth',
protoPath: join(__dirname, '../../../proto/auth.proto'),
url: configService.getOrThrow<string>('AUTH_GRPC_URL'),
},
}),
inject: [ConfigService],
},
{
name: SWAP_SERVICE_NAME,
useFactory: (configService: ConfigService) => ({
Expand Down Expand Up @@ -115,20 +132,22 @@ import { SolowalletController } from './solowallet/solowallet.controller';
]),
],
controllers: [
AdminController,
AuthController,
SwapController,
NostrController,
SmsController,
SharesController,
SolowalletController,
AdminController,
],
providers: [
AuthService,
SwapService,
NostrService,
SmsService,
SharesService,
AdminService,
SolowalletService,
AdminService,
],
})
export class ApiModule {}
39 changes: 39 additions & 0 deletions apps/api/src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { TestingModule } from '@nestjs/testing';
import { createTestingModuleWithValidation } from '@bitsacco/testing';

import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';

describe('AuthController', () => {
let controller: AuthController;
let authService: AuthService;

beforeEach(async () => {
const module: TestingModule = await createTestingModuleWithValidation({
controllers: [AuthController],
providers: [
ConfigService,
{
provide: AuthService,
useValue: {
loginUser: jest.fn(),
registerUser: jest.fn(),
verifyUser: jest.fn(),
authenticate: jest.fn(),
},
},
JwtService,
],
});

controller = module.get<AuthController>(AuthController);
authService = module.get<AuthService>(AuthService);
});

it('should be defined', () => {
expect(controller).toBeDefined();
expect(authService).toBeDefined();
});
});
90 changes: 90 additions & 0 deletions apps/api/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { type Response } from 'express';
import { firstValueFrom, Observable } from 'rxjs';
import { Body, Controller, Logger, Post, Res } from '@nestjs/common';
import { ApiBody, ApiOperation } from '@nestjs/swagger';
import { JwtService } from '@nestjs/jwt';
import {
AuthRequestDto,
AuthResponse,
AuthTokenPayload,
LoginUserRequestDto,
RegisterUserRequestDto,
VerifyUserRequestDto,
} from '@bitsacco/common';
import { AuthService } from './auth.service';

@Controller('auth')
export class AuthController {
private readonly logger = new Logger(AuthController.name);

constructor(
private readonly authService: AuthService,
private readonly jwtService: JwtService,
) {
this.logger.log('AuthController initialized');
}

@Post('login')
@ApiOperation({ summary: 'Login user' })
@ApiBody({
type: LoginUserRequestDto,
})
async login(
@Body() req: LoginUserRequestDto,
@Res({ passthrough: true }) res: Response,
) {
const auth = this.authService.loginUser(req);
return this.setAuthCookie(auth, res);
}

@Post('register')
@ApiOperation({ summary: 'Register user' })
@ApiBody({
type: RegisterUserRequestDto,
})
register(@Body() req: RegisterUserRequestDto) {
return firstValueFrom(this.authService.registerUser(req)).then((user) => ({
user,
}));
}

@Post('verify')
@ApiOperation({ summary: 'Register user' })
@ApiBody({
type: VerifyUserRequestDto,
})
verify(@Body() req: VerifyUserRequestDto) {
return firstValueFrom(this.authService.verifyUser(req)).then((user) => ({
user,
}));
}

@Post('authenticate')
@ApiOperation({ summary: 'Authenticate user' })
@ApiBody({
type: AuthRequestDto,
})
async authenticate(
@Body() req: AuthRequestDto,
@Res({ passthrough: true }) res: Response,
) {
const auth = this.authService.authenticate(req);
return this.setAuthCookie(auth, res);
}

private async setAuthCookie(auth: Observable<AuthResponse>, res: Response) {
return firstValueFrom(auth).then(({ token }) => {
const { user, expires } = this.jwtService.decode<AuthTokenPayload>(token);

res.cookie('Authentication', token, {
httpOnly: true,
expires: new Date(expires),
});

return {
user,
token,
};
});
}
}
34 changes: 34 additions & 0 deletions apps/api/src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { TestingModule } from '@nestjs/testing';
import { AuthServiceClient } from '@bitsacco/common';
import { createTestingModuleWithValidation } from '@bitsacco/testing';
import { ClientGrpc } from '@nestjs/microservices';
import { AuthService } from './auth.service';

describe('AuthService', () => {
let service: AuthService;
let serviceGenerator: ClientGrpc;
let mockAuthServiceClient: Partial<AuthServiceClient>;

beforeEach(async () => {
serviceGenerator = {
getService: jest.fn().mockReturnValue(mockAuthServiceClient),
getClientByServiceName: jest.fn().mockReturnValue(mockAuthServiceClient),
};

const module: TestingModule = await createTestingModuleWithValidation({
providers: [
{
provide: AuthService,
useFactory: () => {
return new AuthService(serviceGenerator);
},
},
],
});
service = module.get<AuthService>(AuthService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
37 changes: 37 additions & 0 deletions apps/api/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {
AUTH_SERVICE_NAME,
AuthRequest,
AuthServiceClient,
LoginUserRequest,
RegisterUserRequest,
VerifyUserRequest,
} from '@bitsacco/common';
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { type ClientGrpc } from '@nestjs/microservices';

@Injectable()
export class AuthService implements OnModuleInit {
private client: AuthServiceClient;

constructor(@Inject(AUTH_SERVICE_NAME) private readonly grpc: ClientGrpc) {}

onModuleInit() {
this.client = this.grpc.getService<AuthServiceClient>(AUTH_SERVICE_NAME);
}

loginUser(req: LoginUserRequest) {
return this.client.loginUser(req);
}

registerUser(req: RegisterUserRequest) {
return this.client.registerUser(req);
}

verifyUser(req: VerifyUserRequest) {
return this.client.verifyUser(req);
}

authenticate(req: AuthRequest) {
return this.client.authenticate(req);
}
}
2 changes: 1 addition & 1 deletion apps/auth/.dev.env
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ AUTH_GRPC_URL='auth:4010'
DATABASE_URL=mongodb://bs:password@mongodb:27017
JWT_SECRET='secret'
JWT_EXPIRATION='3600'
PIN_SALT='BSPN'
SALT_ROUNDS='12'
2 changes: 1 addition & 1 deletion apps/auth/src/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class AuthController {
}

@GrpcMethod()
verifyuser(req: VerifyUserRequestDto) {
verifyUser(req: VerifyUserRequestDto) {
return this.authService.verifyUser(req);
}

Expand Down
2 changes: 1 addition & 1 deletion apps/auth/src/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { AuthService } from './auth.service';
DATABASE_URL: Joi.string().required(),
JWT_SECRET: Joi.string().required(),
JWT_EXPIRATION: Joi.string().required(),
PIN_SALT: Joi.string().required(),
SALT_ROUNDS: Joi.number().required(),
}),
}),
DatabaseModule,
Expand Down
11 changes: 4 additions & 7 deletions apps/auth/src/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,14 @@ import { ConfigService } from '@nestjs/config';
import { Injectable, Logger } from '@nestjs/common';
import {
AuthRequest,
AuthTokenPayload,
LoginUserRequestDto,
RegisterUserRequestDto,
User,
VerifyUserRequestDto,
} from '@bitsacco/common';
import { UsersService } from './users';

interface AuthTokenPayload {
user: User;
expires: Date;
}

@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);
Expand All @@ -29,8 +25,9 @@ export class AuthService {

async loginUser(req: LoginUserRequestDto) {
const user = await this.userService.validateUser(req);

return this.createAuthToken(user);
return {
token: this.createAuthToken(user),
};
}

async registerUser(req: RegisterUserRequestDto) {
Expand Down
Loading

0 comments on commit 4b845e0

Please sign in to comment.