Skip to content

Commit

Permalink
feat: add auth login api
Browse files Browse the repository at this point in the history
  • Loading branch information
hqwuzhaoyi committed Oct 10, 2023
1 parent 7605bbb commit a21ff82
Show file tree
Hide file tree
Showing 16 changed files with 531 additions and 0 deletions.
1 change: 1 addition & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@nestjs/common": "^10.2.7",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.2.7",
"@nestjs/jwt": "^10.1.1",
"@nestjs/mapped-types": "^2.0.2",
"@nestjs/platform-express": "^10.2.7",
"@nestjs/platform-socket.io": "^10.2.7",
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { StaticDirModule } from "./static-dir.provider";
import { BullBoardModule } from "@bull-board/nestjs";
import { ExpressAdapter } from "@bull-board/express";
import { SubtitleModule } from './subtitle/subtitle.module';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';

import * as fs from "fs-extra";
import * as path from "path";
Expand Down Expand Up @@ -78,6 +80,8 @@ const rootPath = path.join(__dirname, "..", "..", "..");
SharedModule,
StaticDirModule,
SubtitleModule,
AuthModule,
UsersModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
66 changes: 66 additions & 0 deletions apps/server/src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Test, TestingModule } from "@nestjs/testing";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { RegisterDto } from "./dto/register.dto";
import { UsersService } from "@/users/users.service";
import { JwtService } from "@nestjs/jwt";
import { getRepositoryToken } from "@nestjs/typeorm";
import { User } from "@/users/users.entity";

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

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
providers: [
AuthService,
UsersService,
JwtService,
{
provide: getRepositoryToken(User), // Replace 'VideoFileEntity' with your actual entity name
useValue: {}, // Mock the repository methods you need
},
],
}).compile();

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

describe("signIn", () => {
it("should return a token", async () => {
const signInDto: RegisterDto = {
username: "testuser",
password: "testpassword",
};
const token = "testtoken";
jest.spyOn(authService, "signIn").mockImplementation(async () => token);

expect(await controller.signIn(signInDto)).toBe(token);
});
});

describe("register", () => {
it("should return a user", async () => {
const registerDto: RegisterDto = {
username: "testuser",
password: "testpassword",
};
const user = { id: 1, username: "testuser", password: "testpassword" };
jest
.spyOn(authService, "register")
.mockImplementation(async () => Promise.resolve(user));

expect(await controller.register(registerDto)).toBe(user);
});
});

describe("getProfile", () => {
it("should return the authenticated user", () => {
const user = { id: 1, username: "testuser" };
expect(controller.getProfile({ user })).toBe(user);
});
});
});
38 changes: 38 additions & 0 deletions apps/server/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Request,
UseGuards,
} from "@nestjs/common";
import { AuthGuard } from "./auth.guard";
import { AuthService } from "./auth.service";
import { Public } from "./decorators/public.decorator";
import { RegisterDto } from "./dto/register.dto";

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

@Public()
@HttpCode(HttpStatus.OK)
@Post("login")
signIn(@Body() signInDto: RegisterDto) {
return this.authService.signIn(signInDto.username, signInDto.password);
}

@Public()
@Post("register")
async register(@Body() registerDto: RegisterDto) {
return this.authService.register(registerDto);
}

@UseGuards(AuthGuard)
@Get("profile")
getProfile(@Request() req) {
return req.user;
}
}
52 changes: 52 additions & 0 deletions apps/server/src/auth/auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { jwtConstants } from "./constants";
import { Request } from "express";
import { Reflector } from "@nestjs/core";
import { IS_PUBLIC_KEY } from "./decorators/public.decorator";

@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private reflector: Reflector
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
// 💡 See this condition
return true;
}

const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: jwtConstants.secret,
});
// 💡 We're assigning the payload to the request object here
// so that we can access it in our route handlers
request["user"] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}

private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(" ") ?? [];
return type === "Bearer" ? token : undefined;
}
}
28 changes: 28 additions & 0 deletions apps/server/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Module } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { AuthController } from "./auth.controller";
import { UsersModule } from "../users/users.module";
import { jwtConstants } from "./constants";
import { JwtModule } from "@nestjs/jwt";
import { AuthGuard } from "./auth.guard";
import { APP_GUARD } from "@nestjs/core";

@Module({
imports: [
UsersModule,
JwtModule.register({
global: true,
secret: jwtConstants.secret,
signOptions: { expiresIn: "60s" },
}),
],
providers: [
AuthService,
{
provide: APP_GUARD,
useClass: AuthGuard,
},
],
controllers: [AuthController],
})
export class AuthModule {}
116 changes: 116 additions & 0 deletions apps/server/src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Test, TestingModule } from "@nestjs/testing";
import { AuthService } from "./auth.service";
import { UsersService } from "@/users/users.service";
import { JwtService } from "@nestjs/jwt";
import { getRepositoryToken } from "@nestjs/typeorm";
import { User } from "@/users/users.entity";
import { UnauthorizedException } from "@nestjs/common";
import { RegisterDto } from "./dto/register.dto";

describe("AuthService", () => {
let authService: AuthService;
let usersService: UsersService;
let jwtService: JwtService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{
provide: UsersService,
useValue: {
findOne: jest.fn(),
register: jest.fn(),
},
},
{
provide: JwtService,
useValue: {
signAsync: jest.fn(),
},
},
{
provide: getRepositoryToken(User), // Replace 'VideoFileEntity' with your actual entity name
useValue: {}, // Mock the repository methods you need
},
],
}).compile();

authService = module.get<AuthService>(AuthService);
usersService = module.get<UsersService>(UsersService);
jwtService = module.get<JwtService>(JwtService);
});

it("should be defined", () => {
expect(authService).toBeDefined();
});

describe("signIn", () => {
it("should throw UnauthorizedException if user is not found", async () => {
jest.spyOn(usersService, "findOne").mockResolvedValueOnce(undefined);

await expect(authService.signIn("username", "password")).rejects.toThrow(
UnauthorizedException
);
});

it("should throw UnauthorizedException if password is incorrect", async () => {
jest.spyOn(usersService, "findOne").mockResolvedValueOnce({
id: 1,
username: "username",
password: "password",
});

await expect(
authService.signIn("username", "incorrect-password")
).rejects.toThrow(UnauthorizedException);
});

it("should return access token if user is found and password is correct", async () => {
const user = {
id: 1,
username: "username",
password: "password",
};
jest.spyOn(usersService, "findOne").mockResolvedValueOnce(user);
jest.spyOn(jwtService, "signAsync").mockResolvedValueOnce("access-token");

const result = await authService.signIn("username", "password");

expect(result).toEqual({ access_token: "access-token" });
expect(jwtService.signAsync).toHaveBeenCalledWith({
sub: user.id,
username: user.username,
});
});
});

describe("register", () => {
it("should call usersService.register with correct arguments", async () => {
const registerDto: RegisterDto = {
username: "username",
password: "password",
};

await authService.register(registerDto);

expect(usersService.register).toHaveBeenCalledWith(
registerDto.username,
registerDto.password
);
});

it("should return the result of usersService.register", async () => {
const result = { id: 1, username: "username", password: "password" };
jest.spyOn(usersService, "register").mockResolvedValueOnce(result);

const registerDto: RegisterDto = {
username: "username",
password: "password",
};
const response = await authService.register(registerDto);

expect(response).toEqual(result);
});
});
});
29 changes: 29 additions & 0 deletions apps/server/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { UsersService } from "../users/users.service";
import { JwtService } from "@nestjs/jwt";
import { RegisterDto } from "./dto/register.dto";

@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService
) {}

async signIn(username: string, pass: string): Promise<any> {
const user = await this.usersService.findOne(username);
if (user?.password !== pass) {
throw new UnauthorizedException();
}
// instead of the user object
const payload = { sub: user.id, username: user.username };
return {
access_token: await this.jwtService.signAsync(payload),
};
}

async register(registerDto: RegisterDto) {
const { username, password } = registerDto;
return this.usersService.register(username, password);
}
}
4 changes: 4 additions & 0 deletions apps/server/src/auth/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const jwtConstants = {
secret:
"DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.",
};
4 changes: 4 additions & 0 deletions apps/server/src/auth/decorators/public.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
24 changes: 24 additions & 0 deletions apps/server/src/auth/dto/register.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
IsString,
IsEmail,
MinLength,
IsBoolean,
Equals,
} from "class-validator";

export class RegisterDto {
@IsString()
@MinLength(6)
password: string;

// @IsString()
// @MinLength(6)
// @Equals("password")
// confirmPassword: string;

@IsString()
username: string;

// @IsBoolean()
// termsAndConditionsAccepted: boolean;
}
Loading

0 comments on commit a21ff82

Please sign in to comment.