Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/local user login #10

Merged
merged 59 commits into from
Sep 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
cb2993a
feat: setup local passport
a20688392 Jul 3, 2023
b7e9c10
feat: apply local passport & setup loccal strategy
a20688392 Jul 3, 2023
3b0338f
feat: apply local auth guard & user login controller
a20688392 Jul 3, 2023
05b7cdb
feat: setup passport-jwt
a20688392 Jul 3, 2023
7fb4d28
feat: setup jwt env value & check whether jwt env is set during initi…
a20688392 Jul 3, 2023
34cd351
refactor: put insensitive user information into jwt
a20688392 Jul 3, 2023
d3ebf77
refactor: set config as global variables
a20688392 Jul 3, 2023
9cd96d0
feat: setup jwt settings
a20688392 Jul 4, 2023
e11442d
feat: apply jwt when login return jwt token
a20688392 Jul 4, 2023
acc219a
feat: setting default jwt secert for jest test
a20688392 Jul 4, 2023
cd10c00
feat: setup jest test need modules
a20688392 Jul 4, 2023
bf05f48
feat: add swagger description
a20688392 Jul 4, 2023
51c613c
feat: make local auth guard test
a20688392 Jul 4, 2023
69a3719
feat: make localStrategy test
a20688392 Jul 4, 2023
5839b81
feat: make jwt auth guard test
a20688392 Jul 4, 2023
06142ba
feat: make auth service test
a20688392 Jul 5, 2023
2f29f49
refactor: reduce unnecessary code
a20688392 Jul 5, 2023
c41516a
refactor: reduce unnecessary return information
a20688392 Jul 5, 2023
91abbda
feat: make auth controller test login route
a20688392 Jul 5, 2023
25dcd61
refactor: revise the filename definition which is use for user login …
a20688392 Jul 9, 2023
da452df
refactor: set mock environment variable
a20688392 Jul 9, 2023
23ca097
refactor: jest test use mock environment variable
a20688392 Jul 9, 2023
7f67d47
feat: add JWT_SECRET .env.example
a20688392 Jul 10, 2023
4dbb6e9
refactor: revised that Response instead of Respose
a20688392 Jul 10, 2023
efd5326
refactor: revised grammar questions about words
a20688392 Jul 10, 2023
ca0d391
refactor: revised the error message of IsNotEmpty in login-user.dto.ts
a20688392 Jul 10, 2023
067c46e
fix: restore proper authentication
a20688392 Jul 10, 2023
a710629
refactor: revised recommend in auth.service.ts
a20688392 Jul 10, 2023
42a70d1
fix: delete email field in jwt payload
a20688392 Jul 10, 2023
b106b16
fix: delete default secret
a20688392 Jul 10, 2023
43a0e95
fix: use forbidden when account information is wrong
a20688392 Jul 10, 2023
658517f
refactor: make fake access token as a variable
a20688392 Jul 10, 2023
e8557f8
feat: add unit test for non-existent account and wrong password
a20688392 Jul 10, 2023
cc9b1ff
refactor: add type devDependencies about passport
a20688392 Jul 11, 2023
1cb8a20
fix: fix authController request type error
a20688392 Jul 11, 2023
ba5d2aa
refactor: change jwt* to jwtAccess*
a20688392 Jul 11, 2023
9832549
test: revised to match this functional description in localAuthGuard
a20688392 Jul 11, 2023
79bbde5
refactor: remove meanless comment
a20688392 Aug 6, 2023
56a6880
refactor: delete jwtAccessConfigJest use by jwtAccessConfig
a20688392 Aug 6, 2023
7398d7a
refactor: check all errors first and only respond when everything is …
a20688392 Aug 6, 2023
66ecf2b
refactor: revised auth login api response description
a20688392 Aug 6, 2023
118f301
refactor: revised spelling mistakes
a20688392 Aug 6, 2023
a61084e
test: change to httpException because the test will always passed due…
a20688392 Aug 7, 2023
84d6b10
feat: add refresh token secret setting
a20688392 Aug 8, 2023
e97065c
feat: add auth refresh api
a20688392 Aug 8, 2023
b99cdd1
test: delete jwtAccessConfig
a20688392 Aug 8, 2023
4430ec0
test: use time mock to achieve time acceleration
a20688392 Aug 9, 2023
ebbce93
test: add refresh token unit test
a20688392 Aug 9, 2023
04ea6fd
feat: add validation field information in LocalAuthGuard
a20688392 Aug 9, 2023
5af6c14
test: add validation field information in LocalAuthGuard
a20688392 Aug 9, 2023
884f2fd
test: add a user found in UserService
a20688392 Aug 10, 2023
b77c2a7
test: add a script to test a file
a20688392 Aug 10, 2023
7a25c6a
test: add unit test coverage inspection scope
a20688392 Aug 10, 2023
a2cc355
feat: enable BearerAuth functionality for Swagger
a20688392 Aug 10, 2023
35ac46c
feat: add Swagger documentation for the refresh token API
a20688392 Aug 10, 2023
42c5054
refactor: correct spelling which is BadRequest
a20688392 Aug 16, 2023
7f8912e
refactor: add swagger securitySchemes
a20688392 Aug 25, 2023
4e191d8
test: optimize the code according to the suggestions
a20688392 Aug 31, 2023
ee6b2d1
refactor: revise the document according to the OpenAPI specifications
a20688392 Sep 1, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ DB_PASSWORD=secret
DB_DATABASE=cophr
DB_TIMEZONE="+08:00"

JWT_ACCESS_SECRET=
JWT_REFRESH_SECRET=

APP_SWAGGER_Title="NestJS-Board API"
APP_SWAGGER_Description="This is NestJS-Board API documentation."
APP_SWAGGER_Version="0.0.1"
15 changes: 14 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"test": "jest --verbose",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:one": "jest --testPathPattern",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"typeorm": "npm run build && npx typeorm -d ./dist/config/data-source.js",
Expand All @@ -36,7 +37,9 @@
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.2.0",
"@nestjs/core": "^9.0.0",
"@nestjs/jwt": "^10.1.0",
"@nestjs/mapped-types": "*",
"@nestjs/passport": "^10.0.0",
"@nestjs/platform-express": "^9.0.0",
"@nestjs/swagger": "^6.1.4",
"@nestjs/typeorm": "^9.0.1",
Expand All @@ -45,6 +48,9 @@
"class-validator": "^0.14.0",
"dotenv": "^16.0.3",
"mysql2": "^2.3.3",
"passport": "^0.6.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
Expand All @@ -61,6 +67,9 @@
"@types/express": "^4.17.13",
"@types/jest": "28.1.8",
"@types/node": "^16.0.0",
"@types/passport": "^1.0.12",
"@types/passport-jwt": "^3.0.8",
"@types/passport-local": "^1.0.35",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
Expand Down Expand Up @@ -95,7 +104,11 @@
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
"**/*.(t|j)s",
"!**/swagger/**",
"!**/database/**",
"!**/main.ts",
"!**/config/**"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node",
Expand Down
3 changes: 3 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { TypeOrmModule } from "@nestjs/typeorm";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { AuthModule } from "./auth/auth.module";
import appConfig from "./config/app.config";
import { dataSourceOptions } from "./config/data-source";
import { validate } from "./config/env.validation";
import { UserModule } from "./user/user.module";
Expand All @@ -13,6 +14,8 @@ import { UserModule } from "./user/user.module";
controllers: [AppController],
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [appConfig],
validate,
}),
TypeOrmModule.forRoot(dataSourceOptions),
Expand Down
119 changes: 92 additions & 27 deletions src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { type HttpException, ConflictException } from "@nestjs/common";
import { ConflictException } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { type TestingModule, Test } from "@nestjs/testing";
import { getRepositoryToken, TypeOrmModule } from "@nestjs/typeorm";
import { type Request } from "express";
import { dataSourceJest } from "src/config/data-source";
import jestConfig from "src/config/jest.config";
import { UserEntity } from "src/user/entities/user.entity";
import type { CreateUserRespose } from "src/user/resposes/create-user-respose";
import type { CreateUserResponse } from "src/user/responses/create-user-response";
import { UserService } from "src/user/user.service";
import type { Repository } from "typeorm";

import type { CreateUserDto } from "../user/dto/create-user.dto";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { type JwtUser } from "./jwt/jwt.interface";
import { JwtAccessStrategy } from "./jwt/jwt-access.strategy";
import { LocalStrategy } from "./local/local.strategy";
import { type GenerateTokenResponse } from "./responses/generate-token.response";

describe("AuthController", () => {
let authController: AuthController;
Expand All @@ -19,7 +28,14 @@ describe("AuthController", () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
imports: [TypeOrmModule.forRoot(dataSourceJest)],
imports: [
ConfigModule.forRoot({
load: [jestConfig],
}),
PassportModule,
TypeOrmModule.forRoot(dataSourceJest),
JwtModule.register({}),
],
providers: [
AuthService,
UserService,
Expand All @@ -28,6 +44,8 @@ describe("AuthController", () => {
// 使用測試資料庫的 Repository
useValue: UserEntity,
},
LocalStrategy,
JwtAccessStrategy,
],
}).compile();

Expand All @@ -39,43 +57,90 @@ describe("AuthController", () => {
});

describe("create", () => {
const createUserDto: CreateUserDto = {
account: "account",
email: "[email protected]",
name: "displayname",
password: "Password@123",
};
let mockedAuthService: jest.SpyInstance;

beforeEach(async () => {
mockedAuthService = jest.spyOn(authService, "register");
});

it("應該會創建一個使用者,並返回 201 狀態碼", async () => {
const createUserDto: CreateUserDto = {
account: "account",
email: "[email protected]",
name: "displayname",
password: "Password@123",
};
const expectedResponse: CreateUserRespose = {
const expectedResponse: CreateUserResponse = {
message: "創建成功",
statusCode: 201,
};

jest.spyOn(authService, "register").mockResolvedValue(expectedResponse);
mockedAuthService.mockResolvedValue(expectedResponse);

const result = await authController.register(createUserDto);

expect(result).toEqual(expectedResponse);
});

it("應該會發生資料使用者重覆,並返回 409 狀態碼", async () => {
const createUserDto1: CreateUserDto = {
account: "account1",
email: "[email protected]",
name: "displayname",
password: "Password@123",
await authService.register(createUserDto);
await authService.register(createUserDto).catch(error => {
expect(error).toBeInstanceOf(ConflictException);
expect((error as ConflictException).getResponse()).toEqual({
error: "Conflict",
message: ["email 已被註冊。", "account 已被註冊。"],
statusCode: 409,
});
});
});
});

describe("login and refresh", () => {
const request: Request = {
user: {
id: 1,
} as JwtUser,
} as unknown as Request;
const fakeAccessToken = "mocked_access_token";
const fakeRefreshToken = "mocked_refresh_token";
let mockedAuthService: jest.SpyInstance;

beforeEach(async () => {
jest
.spyOn(authService, "generateAccessToken")
.mockReturnValue(Promise.resolve(fakeAccessToken));

jest
.spyOn(authService, "generateRefreshToken")
.mockReturnValue(Promise.resolve(fakeRefreshToken));

mockedAuthService = jest.spyOn(authService, "sign");
});

it("should return access, refresh token and 201 http code when account information is correct.", async () => {
const result = await authController.login(request);

expect(mockedAuthService).toHaveBeenCalledWith(request.user);
const expectedResponse: GenerateTokenResponse = {
accessToken: fakeAccessToken,
refreshToken: fakeRefreshToken,
statusCode: 201,
};

await authService.register(createUserDto1);
await authService
.register(createUserDto1)
.catch((error: HttpException) => {
expect(error).toBeInstanceOf(ConflictException);
expect(error.getResponse()).toEqual({
error: "Conflict",
message: ["email 已被註冊。", "account 已被註冊。"],
statusCode: 409,
});
});
expect(result).toEqual(expectedResponse);
});

it("should return access, refresh token and 201 http code when refresh token is correct.", async () => {
const result = await authController.refresh(request);

expect(mockedAuthService).toHaveBeenCalledWith(request.user);
const expectedResponse: GenerateTokenResponse = {
accessToken: fakeAccessToken,
refreshToken: fakeRefreshToken,
statusCode: 201,
};

expect(result).toEqual(expectedResponse);
});
});

Expand Down
70 changes: 63 additions & 7 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
import { Body, Controller, Post } from "@nestjs/common";
import { Body, Controller, Get, Post, Req, UseGuards } from "@nestjs/common";
import {
ApiBadRequestResponse,
ApiBearerAuth,
ApiBody,
ApiConflictResponse,
ApiCreatedResponse,
ApiForbiddenResponse,
ApiOperation,
ApiTags,
ApiUnauthorizedResponse,
} from "@nestjs/swagger";
import { Request } from "express";
import { BadRequestError } from "src/error/bad-request-error";
import { ConflictError } from "src/error/conflict-error";
import { ForbiddenError } from "src/error/forbidden-error";
import { UnauthorizedError } from "src/error/unauthorized-error";
import { CreateUserDto } from "src/user/dto/create-user.dto";
import { CreateUserBadrequestError } from "src/user/exceptions/create-user-badrequest-error.exception";
import { CreateUserConflictError } from "src/user/exceptions/create-user-conflict-error.exception";
import { CreateUserRespose } from "src/user/resposes/create-user-respose";
import { LoginUserDto } from "src/user/dto/login-user.dto";
import { CreateUserResponse } from "src/user/responses/create-user-response";

import { AuthService } from "./auth.service";
import { type JwtUser } from "./jwt/jwt.interface";
import { JwtRefreshGuard } from "./jwt/jwt-refresh.guard";
import { LocalAuthGuard } from "./local/local-auth.guard";
import { GenerateTokenResponse } from "./responses/generate-token.response";

@ApiTags("Auth")
@Controller("auth")
Expand All @@ -25,17 +37,61 @@ export class AuthController {
})
@ApiCreatedResponse({
description: "使用者創建成功",
type: CreateUserRespose,
type: CreateUserResponse,
})
@ApiConflictResponse({
description: "使用者資料重覆",
type: CreateUserConflictError,
type: ConflictError,
})
@ApiBadRequestResponse({
description: "使用者格式不符",
type: CreateUserBadrequestError,
type: BadRequestError,
})
async register(@Body() userDto: CreateUserDto) {
return this.authService.register(userDto);
}

@Post("login")
@UseGuards(LocalAuthGuard)
@ApiOperation({
description:
"Will return access and refresh token when the account information is correct.",
summary: "login with our account",
})
@ApiCreatedResponse({
description: "Success with generated token",
type: GenerateTokenResponse,
})
@ApiForbiddenResponse({
description: "Account information error",
type: ForbiddenError,
})
@ApiBody({ type: LoginUserDto })
async login(@Req() request: Request) {
return this.authService.sign(request.user as JwtUser);
}

@Get("refresh")
@ApiBearerAuth()
@UseGuards(JwtRefreshGuard)
@ApiOperation({
description:
"Will return access and refresh token when the refresh token carried in the request is valid.",
summary: "refresh token",
})
@ApiCreatedResponse({
description: "Success with generated token",
type: GenerateTokenResponse,
})
@ApiUnauthorizedResponse({
description: String(
"Token error \n " +
"When the token is not carried in the request. \n" +
"When the token has expired or become invalid.",
),
type: UnauthorizedError,
})
async refresh(@Req() request: Request) {
return this.login(request);
}
}
19 changes: 17 additions & 2 deletions src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { TypeOrmModule } from "@nestjs/typeorm";
import { UserEntity } from "src/user/entities/user.entity";
import { UserModule } from "src/user/user.module";

import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { JwtAccessStrategy } from "./jwt/jwt-access.strategy";
import { JwtRefreshStrategy } from "./jwt/jwt-refresh.strategy";
import { LocalStrategy } from "./local/local.strategy";

@Module({
controllers: [AuthController],
imports: [UserModule, TypeOrmModule.forFeature([UserEntity])],
providers: [AuthService],
imports: [
UserModule,
PassportModule,
TypeOrmModule.forFeature([UserEntity]),
JwtModule.register({}),
],
providers: [
AuthService,
LocalStrategy,
JwtAccessStrategy,
JwtRefreshStrategy,
],
})
export class AuthModule {}
Loading