Skip to content

Commit

Permalink
feat: 유저 로그인 및 인증 구현 (테스트 문제 있음)
Browse files Browse the repository at this point in the history
  • Loading branch information
minjungw00 committed Nov 14, 2024
1 parent 5edb57a commit c56fc30
Show file tree
Hide file tree
Showing 15 changed files with 704 additions and 5 deletions.
4 changes: 2 additions & 2 deletions client/src/features/page/hooks/usePage.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useIsSidebarOpen } from "@src/stores/useSidebarStore";
import { Position, Size } from "@src/types/page";
import { useEffect, useState } from "react";
import { PAGE, SIDE_BAR } from "@constants/size";
import { SPACING } from "@constants/spacing";
import { useIsSidebarOpen } from "@src/stores/useSidebarStore";
import { Position, Size } from "@src/types/page";

const PADDING = SPACING.MEDIUM * 2;

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"lint": "eslint . --fix",
"lint:client": "eslint \"client/src/**/*.{ts,tsx}\" --fix",
"lint:server": "eslint \"server/src/**/*.{ts,tsx}\" --fix",
"build": "cd @noctaCrdt && pnpm build && cd .. && pnpm -r build",
"build": "pnpm build:lib && pnpm -r build",
"build:lib": "cd @noctaCrdt && pnpm build",
"build:client": "cd client && pnpm build",
"build:server": "cd server && pnpm build",
Expand Down
379 changes: 379 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion server/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ const config: Config = {
rootDir: ".",
testRegex: ".*\\.spec\\.ts$",
transform: {
"^.+\\.(t|j)s$": "ts-jest",
"^.+\\.(t|j)s$": ["ts-jest", { useESM: true }],
},
collectCoverageFrom: ["**/*.(t|j)s"],
coverageDirectory: "./coverage",
testEnvironment: "node",
preset: "@shelf/jest-mongodb",
watchPathIgnorePatterns: ["globalConfig"],
transformIgnorePatterns: ["node_modules/(?!(nanoid)/)"],
extensionsToTreatAsEsm: [".ts"],
moduleNameMapper: {
"^(\\.{1,2}/.*)\\.js$": "$1",
"^nanoid$": require.resolve("nanoid"),
},
};

export default config;
6 changes: 6 additions & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,19 @@
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.3.0",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/mongoose": "^10.1.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-socket.io": "^10.4.7",
"@nestjs/websockets": "^10.4.7",
"@noctaCrdt": "workspace:*",
"bcrypt": "^5.1.1",
"mongodb-memory-server": "^10.1.2",
"mongoose": "^8.8.0",
"nanoid": "^5.0.8",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"socket.io": "^4.8.1"
Expand Down
2 changes: 2 additions & 0 deletions server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { MongooseModule } from "@nestjs/mongoose";
import { AuthModule } from './auth/auth.module';

Check failure on line 6 in server/src/app.module.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

Replace `'./auth/auth.module'` with `"./auth/auth.module"`

@Module({
imports: [
Expand All @@ -19,6 +20,7 @@ import { MongooseModule } from "@nestjs/mongoose";
uri: configService.get<string>("MONGO_URI"), // 환경 변수에서 MongoDB URI 가져오기
}),
}),
AuthModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
52 changes: 52 additions & 0 deletions server/src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Test, TestingModule } from "@nestjs/testing";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";

jest.mock("nanoid", () => ({
nanoid: jest.fn(() => "mockNanoId123"),
}));

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

const mockAuthService = {
register: jest.fn(),
login: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
providers: [{ provide: AuthService, useValue: mockAuthService }],
}).compile();

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

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

describe("register", () => {
it("should call authService.register and return the result", async () => {
const dto = {
email: "[email protected]",
password: "password123",
name: "Test User",
};
const mockResult = {
id: "mockNanoId123",
email: "[email protected]",
name: "Test User",
};
mockAuthService.register.mockResolvedValue(mockResult);

const result = await authController.register(dto);

expect(authService.register).toHaveBeenCalledWith(dto.email, dto.password, dto.name);
expect(result).toEqual(mockResult);
});
});
});
29 changes: 29 additions & 0 deletions server/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Controller, Post, Body, Request, UseGuards } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { JwtAuthGuard } from "./jwt-auth.guard";

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

@Post("register")
async register(@Body() body: { email: string; password: string; name: string }) {
const { email, password, name } = body;
return this.authService.register(email, password, name);
}

@Post("login")
async login(@Body() body: { email: string; password: string }) {
const user = await this.authService.validateUser(body.email, body.password);
if (!user) {
throw new Error("Invalid credentials");
}
return this.authService.login(user);
}

@UseGuards(JwtAuthGuard)
@Post("profile")
getProfile(@Request() req) {
return req.user;
}
}
22 changes: 22 additions & 0 deletions server/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { User, UserSchema } from "./schemas/user.schema";
import { AuthService } from "./auth.service";
import { AuthController } from "./auth.controller";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { JwtStrategy } from "./jwt.strategy";

@Module({
imports: [
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: "1h" },
}),
],
providers: [AuthService, JwtStrategy],
controllers: [AuthController],
})
export class AuthModule {}
119 changes: 119 additions & 0 deletions server/src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Test, TestingModule } from "@nestjs/testing";
import { AuthService } from "./auth.service";
import { getModelToken } from "@nestjs/mongoose";
import { JwtService } from "@nestjs/jwt";
import * as bcrypt from "bcrypt";
import { nanoid } from "nanoid";

jest.mock("nanoid", () => ({
nanoid: jest.fn(() => "mockNanoId123"),
}));

jest.mock("bcrypt", () => ({
hash: jest.fn(() => Promise.resolve("hashedPassword123")),
compare: jest.fn(() => Promise.resolve(true)),
}));

describe("AuthService", () => {
let authService: AuthService;
let userModel: any;

Check warning on line 19 in server/src/auth/auth.service.spec.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

Unexpected any. Specify a different type
let jwtService: JwtService;

const mockUserModel = {
create: jest.fn(),
findOne: jest.fn(),
};

const mockJwtService = {
sign: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{ provide: getModelToken("User"), useValue: mockUserModel },
{ provide: JwtService, useValue: mockJwtService },
],
}).compile();

authService = module.get<AuthService>(AuthService);
userModel = module.get(getModelToken("User"));
jwtService = module.get<JwtService>(JwtService);
});

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

describe("register", () => {
it("should create a new user with a nanoid as id", async () => {
// nanoid mock 설정
const mockId = "mockNanoId123";
(nanoid as jest.Mock).mockReturnValue(mockId);

const hashedPassword = "hashedPassword123";
jest.spyOn(bcrypt, "hash").mockResolvedValue(hashedPassword);

const mockUser = {
id: mockId,
email: "[email protected]",
password: hashedPassword,
name: "Test User",
};

userModel.create.mockResolvedValue(mockUser);

const result = await authService.register("[email protected]", "password123", "Test User");

expect(nanoid).toHaveBeenCalled(); // nanoid 호출 확인
expect(userModel.create).toHaveBeenCalledWith({
id: mockId,
email: "[email protected]",
password: hashedPassword,
name: "Test User",
});
expect(result).toEqual(mockUser);
});
});

describe("validateUser", () => {
it("should return the user if credentials are valid", async () => {
const mockUser = {
id: "mockNanoId123",
email: "[email protected]",
password: "hashedPassword123",
};
userModel.findOne.mockResolvedValue(mockUser);
jest.spyOn(bcrypt, "compare").mockResolvedValue(true);

const result = await authService.validateUser("[email protected]", "password123");

expect(userModel.findOne).toHaveBeenCalledWith({ email: "[email protected]" });
expect(result).toEqual(mockUser);
});

it("should return null if credentials are invalid", async () => {
userModel.findOne.mockResolvedValue(null);

const result = await authService.validateUser("[email protected]", "password123");

expect(result).toBeNull();
});
});

describe("login", () => {
it("should return a JWT token", async () => {
const mockUser = { id: "mockNanoId123", email: "[email protected]" };
mockJwtService.sign.mockReturnValue("mockJwtToken");

const result = await authService.login(mockUser);

expect(jwtService.sign).toHaveBeenCalledWith({
sub: mockUser.id,
email: mockUser.email,
});
expect(result).toEqual({ accessToken: "mockJwtToken" });
});
});
});
39 changes: 39 additions & 0 deletions server/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model } from "mongoose";
import { User, UserDocument } from "./schemas/user.schema";
import * as bcrypt from "bcrypt";
import { JwtService } from "@nestjs/jwt";

@Injectable()
export class AuthService {
constructor(
@InjectModel(User.name) private userModel: Model<UserDocument>,
private jwtService: JwtService,
) {}

async register(email: string, password: string, name: string): Promise<User> {
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = new this.userModel({
email,
password: hashedPassword,
name,
});
return newUser.save();
}

async validateUser(email: string, password: string): Promise<User | null> {
const user = await this.userModel.findOne({ email });
if (user && (await bcrypt.compare(password, user.password))) {
return user;
}
return null;
}

async login(user: { id: string; email: string }) {
const payload = { sub: user.id, email: user.email };
return {
accessToken: this.jwtService.sign(payload),
};
}
}
5 changes: 5 additions & 0 deletions server/src/auth/jwt-auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";

@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {}
18 changes: 18 additions & 0 deletions server/src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET,
});
}

async validate(payload: any) {

Check warning on line 15 in server/src/auth/jwt.strategy.ts

View workflow job for this annotation

GitHub Actions / Lint and Unit Test

Unexpected any. Specify a different type
return { userId: payload.sub, email: payload.email };
}
}
22 changes: 22 additions & 0 deletions server/src/auth/schemas/user.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Document } from "mongoose";
import { nanoid } from "nanoid";

export type UserDocument = User & Document;

@Schema()
export class User {
@Prop({ required: true, unique: true, default: () => nanoid() })
id: string;

@Prop({ required: true, unique: true })
email: string;

@Prop({ required: true })
password: string;

@Prop({ required: true })
name: string;
}

export const UserSchema = SchemaFactory.createForClass(User);
2 changes: 1 addition & 1 deletion server/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"references": [{ "path": "../@noctaCrdt" }],
"compilerOptions": {
// 기본 설정
"module": "commonjs",
"module": "ESNext",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
Expand Down

0 comments on commit c56fc30

Please sign in to comment.