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 Dec 3, 2024
1 parent d6219a6 commit d2d6bd8
Show file tree
Hide file tree
Showing 37 changed files with 781 additions and 0 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
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);
});
});
12 changes: 12 additions & 0 deletions apps/auth/src/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AuthService } from './auth.service';

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

@Get()
getHello(): string {
return this.authService.getHello();
}
}
12 changes: 12 additions & 0 deletions apps/auth/src/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { UsersService } from './users/users.service';
import { UsersController } from './users/users.controller';

@Module({
imports: [],
controllers: [AuthController, UsersController],
providers: [AuthService, UsersService],
})
export class AuthModule {}
8 changes: 8 additions & 0 deletions apps/auth/src/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';

@Injectable()
export class AuthService {
getHello(): string {
return 'Hello World!';
}
}
3 changes: 3 additions & 0 deletions apps/auth/src/guards/jwt-auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { AuthGuard } from '@nestjs/passport';

export class JwtAuthGuard extends AuthGuard('jwt') {}
3 changes: 3 additions & 0 deletions apps/auth/src/guards/local-auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { AuthGuard } from '@nestjs/passport';

export class LocalAuthGuard extends AuthGuard('local') {}
3 changes: 3 additions & 0 deletions apps/auth/src/interfaces/token-payload.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface TokenPayload {
userId: string;
}
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();
28 changes: 28 additions & 0 deletions apps/auth/src/strategies/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { TokenPayload } from '../interfaces/token-payload.interface';
import { UsersService } from '../users/users.service';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
configService: ConfigService,
private readonly usersService: UsersService,
) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([
(request: any) =>
request?.cookies?.Authentication ||
request?.Authentication ||
request?.headers?.Authentication,
]),
secretOrKey: configService.get('JWT_SECRET'),
});
}

async validate({ userId }: TokenPayload) {
return this.usersService.getUser({ id: userId });
}
}
19 changes: 19 additions & 0 deletions apps/auth/src/strategies/local.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { UsersService } from '../users/users.service';

@Injectable()
export class LocalStategy extends PassportStrategy(Strategy) {
constructor(private readonly usersService: UsersService) {
super({ usernameField: 'email' });
}

async validate(email: string, password: string) {
try {
return await this.usersService.verifyUser(email, password);
} catch (err) {
throw new UnauthorizedException(err);
}
}
}
33 changes: 33 additions & 0 deletions apps/auth/src/users/users.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { TestingModule } from '@nestjs/testing';
import { createTestingModuleWithValidation } from '@bitsacco/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

describe('UsersController', () => {
let controller: UsersController;
let service: UsersService;

beforeEach(async () => {
const module: TestingModule = await createTestingModuleWithValidation({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useValue: {
create: jest.fn(),
validateCreateUserRequestDto: jest.fn(),
verifyUser: jest.fn(),
getUser: jest.fn(),
},
},
],
});

controller = module.get<UsersController>(UsersController);
service = module.get<UsersService>(UsersService);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
24 changes: 24 additions & 0 deletions apps/auth/src/users/users.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
CreateUserRequestDto,
CurrentUser,
UserDocument,
} from '@bitsacco/common';
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}

@Post()
async createUser(@Body() createUserDto: CreateUserRequestDto) {
return this.usersService.create(createUserDto);
}

@Get()
@UseGuards(JwtAuthGuard)
async getUser(@CurrentUser() user: UserDocument) {
return user;
}
}
18 changes: 18 additions & 0 deletions apps/auth/src/users/users.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { DatabaseModule, UserDocument, UserSchema } from '@bitsacco/common';
import { UsersRepository } from './users.repository';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
imports: [
DatabaseModule,
DatabaseModule.forFeature([
{ name: UserDocument.name, schema: UserSchema },
]),
],
controllers: [UsersController],
providers: [UsersService, UsersRepository],
exports: [UsersService],
})
export class UsersModule {}
13 changes: 13 additions & 0 deletions apps/auth/src/users/users.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { Injectable, Logger } from '@nestjs/common';
import { AbstractRepository, UserDocument } from '@bitsacco/common';

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

constructor(@InjectModel(UserDocument.name) userModel: Model<UserDocument>) {
super(userModel);
}
}
35 changes: 35 additions & 0 deletions apps/auth/src/users/users.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { TestingModule } from '@nestjs/testing';
import { createTestingModuleWithValidation } from '@bitsacco/testing';
import { UsersService } from './users.service';
import { UsersRepository } from './users.repository';

describe('UsersService', () => {
let service: UsersService;
let mockUsersRepository: UsersRepository;

beforeEach(async () => {
mockUsersRepository = {
create: jest.fn(),
find: jest.fn(),
findOne: jest.fn(),
findOneAndUpdate: jest.fn(),
findOneAndDelete: jest.fn(),
} as unknown as UsersRepository;

const module: TestingModule = await createTestingModuleWithValidation({
providers: [
UsersService,
{
provide: UsersRepository,
useValue: mockUsersRepository,
},
],
});

service = module.get<UsersService>(UsersService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
Loading

0 comments on commit d2d6bd8

Please sign in to comment.