diff --git a/.github/workflows/publish-auth.yml b/.github/workflows/publish-auth.yml new file mode 100644 index 0000000..8c84947 --- /dev/null +++ b/.github/workflows/publish-auth.yml @@ -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 diff --git a/apps/auth/Dockerfile b/apps/auth/Dockerfile new file mode 100644 index 0000000..a1a653b --- /dev/null +++ b/apps/auth/Dockerfile @@ -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"] diff --git a/apps/auth/src/auth.controller.spec.ts b/apps/auth/src/auth.controller.spec.ts new file mode 100644 index 0000000..35e27a6 --- /dev/null +++ b/apps/auth/src/auth.controller.spec.ts @@ -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); + }); +}); diff --git a/apps/auth/src/auth.controller.ts b/apps/auth/src/auth.controller.ts new file mode 100644 index 0000000..758fe3b --- /dev/null +++ b/apps/auth/src/auth.controller.ts @@ -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(); + } +} diff --git a/apps/auth/src/auth.module.ts b/apps/auth/src/auth.module.ts new file mode 100644 index 0000000..c0f1210 --- /dev/null +++ b/apps/auth/src/auth.module.ts @@ -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 {} diff --git a/apps/auth/src/auth.service.ts b/apps/auth/src/auth.service.ts new file mode 100644 index 0000000..53400c8 --- /dev/null +++ b/apps/auth/src/auth.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AuthService { + getHello(): string { + return 'Hello World!'; + } +} diff --git a/apps/auth/src/guards/jwt-auth.guard.ts b/apps/auth/src/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..47feae7 --- /dev/null +++ b/apps/auth/src/guards/jwt-auth.guard.ts @@ -0,0 +1,3 @@ +import { AuthGuard } from '@nestjs/passport'; + +export class JwtAuthGuard extends AuthGuard('jwt') {} diff --git a/apps/auth/src/guards/local-auth.guard.ts b/apps/auth/src/guards/local-auth.guard.ts new file mode 100644 index 0000000..26a1d73 --- /dev/null +++ b/apps/auth/src/guards/local-auth.guard.ts @@ -0,0 +1,3 @@ +import { AuthGuard } from '@nestjs/passport'; + +export class LocalAuthGuard extends AuthGuard('local') {} diff --git a/apps/auth/src/interfaces/token-payload.interface.ts b/apps/auth/src/interfaces/token-payload.interface.ts new file mode 100644 index 0000000..4f4fc6f --- /dev/null +++ b/apps/auth/src/interfaces/token-payload.interface.ts @@ -0,0 +1,3 @@ +export interface TokenPayload { + userId: string; +} diff --git a/apps/auth/src/main.ts b/apps/auth/src/main.ts new file mode 100644 index 0000000..6a70ab1 --- /dev/null +++ b/apps/auth/src/main.ts @@ -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('AUTH_GRPC_URL'); + const auth = app.connectMicroservice({ + 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(); diff --git a/apps/auth/src/strategies/jwt.strategy.ts b/apps/auth/src/strategies/jwt.strategy.ts new file mode 100644 index 0000000..3caff51 --- /dev/null +++ b/apps/auth/src/strategies/jwt.strategy.ts @@ -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 }); + } +} diff --git a/apps/auth/src/strategies/local.strategy.ts b/apps/auth/src/strategies/local.strategy.ts new file mode 100644 index 0000000..48fe905 --- /dev/null +++ b/apps/auth/src/strategies/local.strategy.ts @@ -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); + } + } +} diff --git a/apps/auth/src/users/users.controller.spec.ts b/apps/auth/src/users/users.controller.spec.ts new file mode 100644 index 0000000..b6d37ba --- /dev/null +++ b/apps/auth/src/users/users.controller.spec.ts @@ -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); + service = module.get(UsersService); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/auth/src/users/users.controller.ts b/apps/auth/src/users/users.controller.ts new file mode 100644 index 0000000..53ffa6b --- /dev/null +++ b/apps/auth/src/users/users.controller.ts @@ -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; + } +} diff --git a/apps/auth/src/users/users.module.ts b/apps/auth/src/users/users.module.ts new file mode 100644 index 0000000..f1766aa --- /dev/null +++ b/apps/auth/src/users/users.module.ts @@ -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 {} diff --git a/apps/auth/src/users/users.repository.ts b/apps/auth/src/users/users.repository.ts new file mode 100644 index 0000000..a4bf077 --- /dev/null +++ b/apps/auth/src/users/users.repository.ts @@ -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 { + protected readonly logger = new Logger(UsersRepository.name); + + constructor(@InjectModel(UserDocument.name) userModel: Model) { + super(userModel); + } +} diff --git a/apps/auth/src/users/users.service.spec.ts b/apps/auth/src/users/users.service.spec.ts new file mode 100644 index 0000000..7516535 --- /dev/null +++ b/apps/auth/src/users/users.service.spec.ts @@ -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); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/auth/src/users/users.service.ts b/apps/auth/src/users/users.service.ts new file mode 100644 index 0000000..5fb15ca --- /dev/null +++ b/apps/auth/src/users/users.service.ts @@ -0,0 +1,48 @@ +import { + Injectable, + UnauthorizedException, + UnprocessableEntityException, +} from '@nestjs/common'; +import * as bcrypt from 'bcryptjs'; +import { UsersRepository } from './users.repository'; +import { CreateUserRequestDto, GetUserDto } from '@bitsacco/common'; + +@Injectable() +export class UsersService { + constructor(private readonly usersRepository: UsersRepository) {} + + async create(createUserDto: CreateUserRequestDto) { + await this.validateCreateUserRequestDto(createUserDto); + return this.usersRepository.create({ + ...createUserDto, + pin: await bcrypt.hash(createUserDto.pin, 10), + phoneVerified: false, + }); + } + + private async validateCreateUserRequestDto( + createUserDto: CreateUserRequestDto, + ) { + try { + await this.usersRepository.findOne({ + where: [{ phone: createUserDto.phone }, { npub: createUserDto.npub }], + }); + } catch (err) { + return; + } + throw new UnprocessableEntityException('User details already exists.'); + } + + async verifyUser(phone: string, pin: string) { + const user = await this.usersRepository.findOne({ phone }); + const pinIsValid = await bcrypt.compare(pin, user.pin); + if (!pinIsValid) { + throw new UnauthorizedException('Credentials are not valid.'); + } + return user; + } + + async getUser({ id }: GetUserDto) { + return this.usersRepository.findOne({ _id: id }); + } +} diff --git a/apps/auth/test/app.e2e-spec.ts b/apps/auth/test/app.e2e-spec.ts new file mode 100644 index 0000000..6eb0624 --- /dev/null +++ b/apps/auth/test/app.e2e-spec.ts @@ -0,0 +1,24 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AuthModule } from './../src/auth.module'; + +describe('AuthController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AuthModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); +}); diff --git a/apps/auth/test/jest-e2e.json b/apps/auth/test/jest-e2e.json new file mode 100644 index 0000000..e9d912f --- /dev/null +++ b/apps/auth/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/apps/auth/tsconfig.app.json b/apps/auth/tsconfig.app.json new file mode 100644 index 0000000..01d9c9a --- /dev/null +++ b/apps/auth/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/auth" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/bun.lockb b/bun.lockb index dec3f49..e9d59cb 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/compose.yml b/compose.yml index 069c3bd..7b83c11 100644 --- a/compose.yml +++ b/compose.yml @@ -15,6 +15,22 @@ services: - '4000:4000' volumes: - .:/usr/src/app + auth: + container_name: auth + build: + context: . + dockerfile: ./apps/auth/Dockerfile + target: development + command: bun dev auth + restart: always + depends_on: + - mongodb + env_file: + - ./apps/auth/.env + ports: + - '4010:4010' + volumes: + - .:/usr/src/app swap: container_name: swap build: diff --git a/libs/common/src/auth/index.ts b/libs/common/src/auth/index.ts new file mode 100644 index 0000000..3525e2d --- /dev/null +++ b/libs/common/src/auth/index.ts @@ -0,0 +1 @@ +export * from './jwt-auth.guard'; diff --git a/libs/common/src/auth/jwt-auth.guard.ts b/libs/common/src/auth/jwt-auth.guard.ts new file mode 100644 index 0000000..c130308 --- /dev/null +++ b/libs/common/src/auth/jwt-auth.guard.ts @@ -0,0 +1,69 @@ +import { + CanActivate, + ExecutionContext, + Inject, + Injectable, + Logger, + OnModuleInit, + UnauthorizedException, +} from '@nestjs/common'; +import { type ClientGrpc } from '@nestjs/microservices'; +import { Reflector } from '@nestjs/core'; +import { catchError, map, Observable, of, tap } from 'rxjs'; +import { AUTH_SERVICE_NAME, AuthServiceClient } from '../types'; + +@Injectable() +export class JwtAuthGuard implements CanActivate, OnModuleInit { + private readonly logger = new Logger(JwtAuthGuard.name); + private authService: AuthServiceClient; + + constructor( + @Inject(AUTH_SERVICE_NAME) private readonly client: ClientGrpc, + private readonly reflector: Reflector, + ) {} + + onModuleInit() { + this.authService = + this.client.getService(AUTH_SERVICE_NAME); + } + + canActivate( + context: ExecutionContext, + ): boolean | Promise | Observable { + const jwt = + context.switchToHttp().getRequest().cookies?.Authentication || + context.switchToHttp().getRequest().headers?.authentication; + + if (!jwt) { + return false; + } + + const roles = this.reflector.get('roles', context.getHandler()); + + return this.authService + .authenticate({ + Authentication: jwt, + }) + .pipe( + tap((res) => { + if (roles) { + for (const role of roles) { + if (!res.roles?.includes(role)) { + this.logger.error('The user does not have valid roles.'); + throw new UnauthorizedException(); + } + } + } + context.switchToHttp().getRequest().user = { + ...res, + _id: res.id, + }; + }), + map(() => true), + catchError((err) => { + this.logger.error(err); + return of(false); + }), + ); + } +} diff --git a/libs/common/src/database/index.ts b/libs/common/src/database/index.ts index aa69c4f..8cb6842 100644 --- a/libs/common/src/database/index.ts +++ b/libs/common/src/database/index.ts @@ -1,3 +1,4 @@ export * from './database.module'; export * from './abstract.repository'; export * from './abstract.schema'; +export * from './user.schema'; diff --git a/libs/common/src/database/user.schema.ts b/libs/common/src/database/user.schema.ts new file mode 100644 index 0000000..6cd87e9 --- /dev/null +++ b/libs/common/src/database/user.schema.ts @@ -0,0 +1,37 @@ +import { AbstractDocument } from '@bitsacco/common'; +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; + +@Schema({ versionKey: false }) +export class UserDocument extends AbstractDocument { + @Prop({ type: String, required: true }) + pin: string; + + @Prop({ type: String, required: false, unique: true }) + phone?: string; + + @Prop({ type: Boolean, required: true }) + phoneVerified: boolean; + + @Prop({ type: String, required: false, unique: true }) + npub?: string; + + @Prop({ type: String, required: false }) + name?: string; + + @Prop({ type: String, required: false }) + avatarUrl?: string; + + @Prop() + roles?: string[]; +} + +export const UserSchema = SchemaFactory.createForClass(UserDocument); + +// Ensure uniqueness only when phone is not null +UserSchema.index( + { + phone: 1, + npub: 1, + }, + { unique: true, sparse: true }, +); diff --git a/libs/common/src/decorators/auth.decorator.ts b/libs/common/src/decorators/auth.decorator.ts new file mode 100644 index 0000000..1cc1977 --- /dev/null +++ b/libs/common/src/decorators/auth.decorator.ts @@ -0,0 +1,17 @@ +import { + createParamDecorator, + ExecutionContext, + SetMetadata, +} from '@nestjs/common'; +import { UserDocument } from '../database'; + +const getCurrentUserByContext = (context: ExecutionContext): UserDocument => { + return context.switchToHttp().getRequest().user; +}; + +export const CurrentUser = createParamDecorator( + (_data: unknown, context: ExecutionContext) => + getCurrentUserByContext(context), +); + +export const Roles = (...roles: string[]) => SetMetadata('roles', roles); diff --git a/libs/common/src/decorators/index.ts b/libs/common/src/decorators/index.ts new file mode 100644 index 0000000..9795c6d --- /dev/null +++ b/libs/common/src/decorators/index.ts @@ -0,0 +1 @@ +export * from './auth.decorator'; diff --git a/libs/common/src/dto/auth.dto.ts b/libs/common/src/dto/auth.dto.ts new file mode 100644 index 0000000..7cdda22 --- /dev/null +++ b/libs/common/src/dto/auth.dto.ts @@ -0,0 +1,38 @@ +import { + IsArray, + IsNotEmpty, + IsOptional, + IsPhoneNumber, + IsString, + Validate, +} from 'class-validator'; +import { CreateUserRequest, IsStringifiedNumberConstraint } from '../types'; +import { Type } from 'class-transformer'; + +export class CreateUserRequestDto implements CreateUserRequest { + @IsPhoneNumber() + @IsOptional() + phone: string; + + @IsNotEmpty() + @IsString() + @Validate(IsStringifiedNumberConstraint) + @Type(() => String) + pin: string; + + @IsString() + @IsOptional() + npub: string; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + @IsNotEmpty({ each: true }) + roles: string[]; +} + +export class GetUserDto { + @IsString() + @IsNotEmpty() + id: string; +} diff --git a/libs/common/src/dto/index.ts b/libs/common/src/dto/index.ts index 662a7fb..69e7e38 100644 --- a/libs/common/src/dto/index.ts +++ b/libs/common/src/dto/index.ts @@ -1,3 +1,4 @@ +export * from './auth.dto'; export * from './lib.dto'; export * from './swap.dto'; export * from './nostr.dto'; diff --git a/libs/common/src/index.ts b/libs/common/src/index.ts index bfea254..7528335 100644 --- a/libs/common/src/index.ts +++ b/libs/common/src/index.ts @@ -1,7 +1,9 @@ +export * from './auth'; export * from './types'; export * from './logger'; export * from './utils'; export * from './constants'; +export * from './decorators'; export * from './dto'; export * from './database'; export * from './fedimint'; diff --git a/libs/common/src/types/index.ts b/libs/common/src/types/index.ts index 051ff10..21928f6 100644 --- a/libs/common/src/types/index.ts +++ b/libs/common/src/types/index.ts @@ -1,3 +1,4 @@ +export * from './proto/auth'; export * from './proto/lib'; export * from './proto/lightning'; export * from './proto/swap'; diff --git a/libs/common/src/types/proto/auth.ts b/libs/common/src/types/proto/auth.ts new file mode 100644 index 0000000..805f77d --- /dev/null +++ b/libs/common/src/types/proto/auth.ts @@ -0,0 +1,84 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// versions: +// protoc-gen-ts_proto v2.2.7 +// protoc v3.21.12 +// source: auth.proto + +/* eslint-disable */ +import { GrpcMethod, GrpcStreamMethod } from '@nestjs/microservices'; +import { Observable } from 'rxjs'; + +export interface CreateUserRequest { + phone: string; + pin: string; + npub: string; + roles: string[]; +} + +export interface AuthUserRequest { + Authentication: string; +} + +export interface User { + id: string; + pin: string; + /** Users phone number identifier */ + phone?: string | undefined; + /** Users phone number has been verified via SMS */ + phoneVerified: boolean; + /** Users nostr npub identifier */ + npub?: string | undefined; + /** Users name or nym */ + name?: string | undefined; + /** Users avatar url */ + avatarUrl?: string | undefined; + /** Users roles */ + roles: string[]; +} + +export interface AuthServiceClient { + createUser(request: CreateUserRequest): Observable; + + authenticate(request: AuthUserRequest): Observable; +} + +export interface AuthServiceController { + createUser( + request: CreateUserRequest, + ): Promise | Observable | User; + + authenticate( + request: AuthUserRequest, + ): Promise | Observable | User; +} + +export function AuthServiceControllerMethods() { + return function (constructor: Function) { + const grpcMethods: string[] = ['createUser', 'authenticate']; + for (const method of grpcMethods) { + const descriptor: any = Reflect.getOwnPropertyDescriptor( + constructor.prototype, + method, + ); + GrpcMethod('AuthService', method)( + constructor.prototype[method], + method, + descriptor, + ); + } + const grpcStreamMethods: string[] = []; + for (const method of grpcStreamMethods) { + const descriptor: any = Reflect.getOwnPropertyDescriptor( + constructor.prototype, + method, + ); + GrpcStreamMethod('AuthService', method)( + constructor.prototype[method], + method, + descriptor, + ); + } + }; +} + +export const AUTH_SERVICE_NAME = 'AuthService'; diff --git a/nest-cli.json b/nest-cli.json index 31b23f4..939542e 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -73,6 +73,15 @@ "compilerOptions": { "tsConfigPath": "apps/solowallet/tsconfig.app.json" } + }, + "auth": { + "type": "application", + "root": "apps/auth", + "entryFile": "main", + "sourceRoot": "apps/auth/src", + "compilerOptions": { + "tsConfigPath": "apps/auth/tsconfig.app.json" + } } }, "monorepo": true, diff --git a/package.json b/package.json index ceeea37..a0a0255 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dev": "nest start --watch", "debug": "nest start --debug --watch", "build": "nest build", + "build:auth": "bun run build auth", "build:api": "bun run build api", "build:swap": "bun run build swap", "build:nostr": "bun run build nostr", @@ -37,13 +38,16 @@ "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.0.0", "@nestjs/event-emitter": "^2.1.1", + "@nestjs/jwt": "^10.2.0", "@nestjs/microservices": "^10.4.5", "@nestjs/mongoose": "^10.1.0", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/sequelize": "^10.0.1", "@nestjs/swagger": "^7.4.2", "@nostr-dev-kit/ndk": "^2.10.6", "africastalking": "^0.7.0", + "bcryptjs": "^2.4.3", "cache-manager": "4.1.0", "cache-manager-redis-store": "^3.0.1", "class-transformer": "^0.5.1", @@ -56,6 +60,9 @@ "mongoose": "^8.8.0", "nestjs-pino": "^4.1.0", "nostr-tools": "^2.10.3", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", "pino-http": "^10.3.0", "pino-pretty": "^11.3.0", "redis": "^4.7.0", @@ -68,10 +75,13 @@ "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", + "@types/bcryptjs": "^2.4.6", "@types/bun": "latest", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", "@types/node": "^22.7.5", + "@types/passport-jwt": "^4.0.1", + "@types/passport-local": "^1.0.38", "@types/sequelize": "^4.28.20", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", diff --git a/proto/auth.proto b/proto/auth.proto new file mode 100644 index 0000000..ba02026 --- /dev/null +++ b/proto/auth.proto @@ -0,0 +1,42 @@ +syntax = "proto3"; + +package auth; + +service AuthService { + rpc CreateUser (CreateUserRequest) returns (User) {} + rpc Authenticate (AuthUserRequest) returns (User) {} +} + +message CreateUserRequest { + string phone = 1; + string pin = 2; + string npub = 3; + repeated string roles = 4; +} + +message AuthUserRequest { + string Authentication = 1; +} + +message User { + string id = 1; + string pin = 2; + + reserved 3, 4, 5; + + // Users phone number identifier + optional string phone = 6; + // Users phone number has been verified via SMS + bool phone_verified = 7; + // Users nostr npub identifier + optional string npub = 8; + // Users name or nym + optional string name = 9; + // Users avatar url + optional string avatar_url = 10; + + reserved 11, 12, 13, 14; + + // Users roles + repeated string roles = 15; +}