Skip to content

Commit

Permalink
feat: init authentication service
Browse files Browse the repository at this point in the history
  • Loading branch information
okjodom committed Jan 13, 2025
1 parent 344c20f commit ed119ba
Show file tree
Hide file tree
Showing 34 changed files with 1,081 additions and 14 deletions.
69 changes: 69 additions & 0 deletions .github/workflows/publish-auth.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: publish-auth

on:
push:
paths:
- apps/auth/**
- libs/**
- package.json
- bun.lockb
- .github/workflows/publish-auth.yml
workflow_dispatch:

jobs:
testing:
uses: ./.github/workflows/wait-for-tests.yml
with:
test-job-name: test

docker:
needs: testing
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v2

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
bitsacco/auth
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- name: Login to Docker Hub
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
uses: docker/login-action@v2
with:
username: okjodom
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}

- name: Build and push auth
uses: docker/build-push-action@v4
with:
file: apps/auth/Dockerfile
push: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

- name: Checkout repository content
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
uses: actions/checkout@v4

# This workflow requires the repository content to be locally available to read the README
- name: Update the Docker Hub description
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
uses: peter-evans/dockerhub-description@v3
with:
username: okjodom
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
repository: bitsacco/auth
readme-filepath: ./apps/auth/README.md
6 changes: 6 additions & 0 deletions apps/auth/.dev.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
NODE_ENV='development'
AUTH_GRPC_URL='auth:4010'
DATABASE_URL=mongodb://bs:password@mongodb:27017
JWT_SECRET='secret'
JWT_EXPIRATION='3600'
PIN_SALT='BSPN'
32 changes: 32 additions & 0 deletions apps/auth/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
FROM oven/bun:latest AS development

WORKDIR /usr/src/app

COPY package.json ./
COPY bun.lockb ./
COPY tsconfig.json tsconfig.json
COPY nest-cli.json nest-cli.json

COPY apps/auth apps/auth
COPY libs libs
COPY proto proto

RUN bun install
RUN bun build:auth

FROM oven/bun:latest AS production

ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}

WORKDIR /usr/src/app

COPY package.json ./
COPY bun.lockb ./

RUN bun install --production

COPY --from=development /usr/src/app/dist ./dist
COPY --from=development /usr/src/app/proto ./proto

CMD ["sh", "-c", "bun run dist/apps/auth/main.js"]
17 changes: 17 additions & 0 deletions apps/auth/src/auth.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { TestingModule } from '@nestjs/testing';
import { createTestingModuleWithValidation } from '@bitsacco/testing';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';

describe('AuthController', () => {
let authController: AuthController;

beforeEach(async () => {
const app: TestingModule = await createTestingModuleWithValidation({
controllers: [AuthController],
providers: [AuthService],
});

authController = app.get<AuthController>(AuthController);
});
});
36 changes: 36 additions & 0 deletions apps/auth/src/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Controller } from '@nestjs/common';
import { AuthService } from './auth.service';
import {
AuthRequestDto,
AuthServiceControllerMethods,
LoginUserRequestDto,
RegisterUserRequestDto,
VerifyUserRequestDto,
} from '@bitsacco/common';
import { GrpcMethod } from '@nestjs/microservices';

@Controller()
@AuthServiceControllerMethods()
export class AuthController {
constructor(private readonly authService: AuthService) {}

@GrpcMethod()
loginUser(req: LoginUserRequestDto) {
return this.authService.loginUser(req);
}

@GrpcMethod()
registerUser(req: RegisterUserRequestDto) {
return this.authService.registerUser(req);
}

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

@GrpcMethod()
authenticate(req: AuthRequestDto) {
return this.authService.authenticate(req);
}
}
47 changes: 47 additions & 0 deletions apps/auth/src/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as Joi from 'joi';
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import {
DatabaseModule,
LoggerModule,
UsersDocument,
UsersSchema,
} from '@bitsacco/common';
import { UsersRepository } from './users/users.repository';
import { UsersService } from './users/users.service';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
NODE_ENV: Joi.string().required(),
AUTH_GRPC_URL: Joi.string().required(),
DATABASE_URL: Joi.string().required(),
JWT_SECRET: Joi.string().required(),
JWT_EXPIRATION: Joi.string().required(),
PIN_SALT: Joi.string().required(),
}),
}),
DatabaseModule,
DatabaseModule.forFeature([
{ name: UsersDocument.name, schema: UsersSchema },
]),
JwtModule.registerAsync({
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: `${configService.get('JWT_EXPIRATION')}s`,
},
}),
inject: [ConfigService],
}),
LoggerModule,
],
controllers: [AuthController],
providers: [AuthService, UsersRepository, UsersService],
})
export class AuthModule {}
71 changes: 71 additions & 0 deletions apps/auth/src/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { Injectable, Logger } from '@nestjs/common';
import {
AuthRequest,
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);

constructor(
private readonly configService: ConfigService,
private readonly userService: UsersService,
private readonly jwtService: JwtService,
) {
this.logger.log('AuthService initialized');
}

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

return this.createAuthToken(user);
}

async registerUser(req: RegisterUserRequestDto) {
return this.userService.registerUser(req);
}

async verifyUser(req: VerifyUserRequestDto) {
return this.userService.verifyUser(req);
}

async authenticate({ token }: AuthRequest) {
const { user, expires } = this.jwtService.verify<AuthTokenPayload>(token);

if (expires < new Date()) {
throw new Error('Token expired. Unauthenticated');
}

const u = await this.userService.findUser({
id: user.id,
});

return this.createAuthToken(u);
}

private createAuthToken(user: User) {
const expires = new Date();
expires.setSeconds(
expires.getSeconds() + this.configService.get('JWT_EXPIRATION'),
);

const payload: AuthTokenPayload = {
user,
expires,
};

return this.jwtService.sign(payload);
}
}
33 changes: 33 additions & 0 deletions apps/auth/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { join } from 'path';
import { Logger } from 'nestjs-pino';
import { NestFactory } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import { ReflectionService } from '@grpc/reflection';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AuthModule } from './auth.module';

async function bootstrap() {
const app = await NestFactory.create(AuthModule);

const configService = app.get(ConfigService);

const auth_url = configService.getOrThrow<string>('AUTH_GRPC_URL');
const auth = app.connectMicroservice<MicroserviceOptions>({
transport: Transport.GRPC,
options: {
package: 'auth',
url: auth_url,
protoPath: join(__dirname, '../../../proto/auth.proto'),
onLoadPackageDefinition: (pkg, server) => {
new ReflectionService(pkg).addToServer(server);
},
},
});

// setup pino logging
app.useLogger(app.get(Logger));

await app.startAllMicroservices();
}

bootstrap();
22 changes: 22 additions & 0 deletions apps/auth/src/strategies/phone.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UsersService } from '../users/users.service';

@Injectable()
export class PhoneStategy extends PassportStrategy(Strategy) {
constructor(private readonly usersService: UsersService) {
super({ usernameField: 'phone', passwordField: 'pinHash' });
}

async validate(phone: string, pin: string) {
try {
return await this.usersService.validateUser({
pin,
phone,
});
} catch (err) {
throw new UnauthorizedException(err);
}
}
}
2 changes: 2 additions & 0 deletions apps/auth/src/users/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './users.repository';
export * from './users.service';
16 changes: 16 additions & 0 deletions apps/auth/src/users/users.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { Injectable, Logger } from '@nestjs/common';
import { AbstractRepository, UsersDocument } from '@bitsacco/common';

@Injectable()
export class UsersRepository extends AbstractRepository<UsersDocument> {
protected readonly logger = new Logger(UsersRepository.name);

constructor(
@InjectModel(UsersDocument.name)
userModel: Model<UsersDocument>,
) {
super(userModel);
}
}
Loading

0 comments on commit ed119ba

Please sign in to comment.