Skip to content

Commit 15ebf1a

Browse files
authored
Merge pull request #5 from wafflestudio/user-api
[Server] User API 구현
2 parents 18c542b + 9817828 commit 15ebf1a

23 files changed

+2781
-166
lines changed

package-lock.json

+2,271-91
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+17-2
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@
2424
"@nestjs/common": "^7.5.1",
2525
"@nestjs/config": "^0.6.3",
2626
"@nestjs/core": "^7.5.1",
27+
"@nestjs/graphql": "^7.9.8",
2728
"@nestjs/jwt": "^7.2.0",
29+
"@nestjs/mapped-types": "^0.3.0",
2830
"@nestjs/passport": "^7.1.5",
2931
"@nestjs/platform-express": "^7.5.1",
32+
"@nestjs/swagger": "^4.7.12",
3033
"@nestjs/typeorm": "^7.1.5",
3134
"@types/bcrypt": "^3.0.0",
3235
"bcrypt": "^5.0.0",
@@ -46,7 +49,7 @@
4649
"@nestjs/cli": "^7.5.1",
4750
"@nestjs/schematics": "^7.1.3",
4851
"@nestjs/testing": "^7.5.1",
49-
"@types/express": "^4.17.8",
52+
"@types/express": "^4.17.11",
5053
"@types/jest": "^26.0.15",
5154
"@types/node": "^14.14.6",
5255
"@types/passport-jwt": "^3.0.4",
@@ -57,8 +60,10 @@
5760
"eslint": "^7.12.1",
5861
"eslint-config-prettier": "7.2.0",
5962
"eslint-plugin-prettier": "^3.1.4",
63+
"husky": "^4.3.8",
6064
"jest": "^26.6.3",
61-
"prettier": "^2.1.2",
65+
"lint-staged": "^10.5.4",
66+
"prettier": "^2.2.1",
6267
"supertest": "^6.0.0",
6368
"ts-jest": "^26.4.3",
6469
"ts-loader": "^8.0.8",
@@ -82,5 +87,15 @@
8287
],
8388
"coverageDirectory": "../coverage",
8489
"testEnvironment": "node"
90+
},
91+
"husky": {
92+
"hooks": {
93+
"pre-commit": "lint-staged"
94+
}
95+
},
96+
"lint-staged": {
97+
"src/**/*.{js,json,ts}": [
98+
"prettier --write"
99+
]
85100
}
86101
}

src/app.module.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { NoticeModule } from './notice/notice.module';
77
import { AuthModule } from './auth/auth.module';
88
import { ConfigModule } from '@nestjs/config';
99
import { TypeOrmModule } from '@nestjs/typeorm';
10-
import { Keyword, User } from './user/user.entity';
10+
import { Keyword, User, UserKeyword } from './user/user.entity';
1111
import { UserNotice, File, Notice } from './notice/notice.entity';
1212
import {
1313
Department,
@@ -41,6 +41,7 @@ const ENV: string = process.env.NODE_ENV;
4141
Tag,
4242
Department,
4343
NoticeTag,
44+
UserKeyword,
4445
],
4546
//need to be set false when production
4647
synchronize: true,

src/auth/auth.guard.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { AuthGuard } from '@nestjs/passport';
2+
import { Injectable } from '@nestjs/common';
3+
4+
@Injectable()
5+
export class JwtAccessGuard extends AuthGuard('jwt-access') {}
6+
7+
@Injectable()
8+
export class AuthTokenGuard extends AuthGuard(['local', 'jwt-refresh']) {}

src/auth/auth.module.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
import { Module } from '@nestjs/common';
22
import { AuthService } from './auth.service';
3+
import { PassportModule } from '@nestjs/passport';
4+
import { JwtModule } from '@nestjs/jwt';
5+
import { JwtAccessStrategy } from './jwt-access.strategy';
6+
import { JwtRefreshStrategy } from './jwt-refresh.strategy';
7+
import { LocalStrategy } from './local.strategy';
38

49
@Module({
5-
providers: [AuthService]
10+
imports: [PassportModule, JwtModule.register({})],
11+
providers: [
12+
AuthService,
13+
LocalStrategy,
14+
JwtAccessStrategy,
15+
JwtRefreshStrategy,
16+
],
17+
exports: [AuthService],
618
})
719
export class AuthModule {}

src/auth/auth.service.spec.ts

-18
This file was deleted.

src/auth/auth.service.ts

+37-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,40 @@
11
import { Injectable } from '@nestjs/common';
2+
import { JwtService } from '@nestjs/jwt';
3+
import * as bcrypt from 'bcrypt';
4+
import { User } from '../user/user.entity';
5+
import { Payload } from '../types/custom-type';
26

37
@Injectable()
4-
export class AuthService {}
8+
export class AuthService {
9+
constructor(private jwtService: JwtService) {}
10+
11+
async validateUser(username: string, password: string): Promise<User> {
12+
const user = await User.findOne({ username });
13+
if (user && (await bcrypt.compare(password, user.password))) {
14+
return user;
15+
}
16+
return null;
17+
}
18+
19+
async login(user: User): Promise<User> {
20+
const payload: Payload = { username: user.username, id: user.id };
21+
22+
const accessToken = this.jwtService.sign(payload, {
23+
secret: process.env.ACCESS_SECRET,
24+
expiresIn: process.env.ACCESS_EXPIRES_IN,
25+
});
26+
const refreshToken = this.jwtService.sign(payload, {
27+
secret: process.env.REFRESH_SECRET,
28+
});
29+
user.refreshToken = await bcrypt.hash(
30+
refreshToken,
31+
+process.env.SALT_ROUND,
32+
);
33+
await User.save(user);
34+
user = await User.findOneWithKeyword({ id: user.id });
35+
user.access_token = accessToken;
36+
user.refresh_token = refreshToken;
37+
38+
return user;
39+
}
40+
}

src/auth/jwt-access.strategy.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { ExtractJwt, Strategy } from 'passport-jwt';
2+
import { Injectable, UnauthorizedException } from '@nestjs/common';
3+
import { PassportStrategy } from '@nestjs/passport';
4+
5+
import { User } from '../user/user.entity';
6+
import { Payload } from '../types/custom-type';
7+
8+
@Injectable()
9+
export class JwtAccessStrategy extends PassportStrategy(
10+
Strategy,
11+
'jwt-access',
12+
) {
13+
constructor() {
14+
super({
15+
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
16+
ignoreExpiration: false,
17+
secretOrKey: process.env.ACCESS_SECRET,
18+
});
19+
}
20+
21+
async validate(payload: Payload): Promise<User> {
22+
const user: User = await User.findOne({ id: payload.id });
23+
if (!user) throw new UnauthorizedException();
24+
return user;
25+
}
26+
}

src/auth/jwt-refresh.strategy.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { ExtractJwt, Strategy } from 'passport-jwt';
2+
import { Injectable, UnauthorizedException } from '@nestjs/common';
3+
import { PassportStrategy } from '@nestjs/passport';
4+
import { Request } from 'express';
5+
import { User } from '../user/user.entity';
6+
import { Payload } from '../types/custom-type';
7+
8+
@Injectable()
9+
export class JwtRefreshStrategy extends PassportStrategy(
10+
Strategy,
11+
'jwt-refresh',
12+
) {
13+
constructor() {
14+
super({
15+
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
16+
secretOrKey: process.env.REFRESH_SECRET,
17+
passReqToCallback: true,
18+
});
19+
}
20+
21+
async validate(request: Request, payload: Payload): Promise<User> {
22+
const refreshToken = request.headers.authorization.replace('Bearer ', '');
23+
const user: User = await User.findOneIfRefreshTokenMatches(
24+
refreshToken,
25+
payload.id,
26+
);
27+
if (!user) throw new UnauthorizedException();
28+
return user;
29+
}
30+
}

src/auth/local.strategy.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Injectable, UnauthorizedException } from '@nestjs/common';
2+
import { PassportStrategy } from '@nestjs/passport';
3+
import { Strategy } from 'passport-local';
4+
import { AuthService } from './auth.service';
5+
import { User } from '../user/user.entity';
6+
7+
@Injectable()
8+
export class LocalStrategy extends PassportStrategy(Strategy) {
9+
constructor(private authService: AuthService) {
10+
super();
11+
}
12+
13+
async validate(username: string, password: string): Promise<User> {
14+
const user: User = await this.authService.validateUser(username, password);
15+
if (!user) {
16+
throw new UnauthorizedException();
17+
}
18+
return user;
19+
}
20+
}

src/types/custom-type.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Request } from 'express';
2+
import { User } from '../user/user.entity';
3+
4+
export class Payload {
5+
username: string;
6+
id: number;
7+
}
8+
9+
export interface UserRequest extends Request {
10+
user?: User;
11+
}

src/user/dto/auth-user.dto.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { IsIn, IsString, Validate, ValidateIf } from 'class-validator';
2+
import { NotDefinedValidator } from '../user.validator';
3+
4+
export class AuthDto {
5+
@IsIn(['password', 'refresh_token'])
6+
grant_type: string;
7+
8+
@ValidateIf((o) => o.grant_type === 'refresh_token')
9+
@Validate(NotDefinedValidator, {
10+
message: 'only refresh_token should be given to the header, not username',
11+
})
12+
username: string;
13+
14+
@ValidateIf((o) => o.grant_type === 'refresh_token')
15+
@Validate(NotDefinedValidator, {
16+
message: 'only refresh_token should be given to the header, not password',
17+
})
18+
password: string;
19+
}

src/user/dto/create-user.dto.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { IsString, Validate } from 'class-validator';
2+
import {
3+
UniqueNicknameValidator,
4+
UniqueUsernameValidator,
5+
} from '../user.validator';
6+
7+
export class CreateUserDto {
8+
@IsString()
9+
@Validate(UniqueUsernameValidator)
10+
readonly username: string;
11+
12+
@IsString()
13+
readonly password: string;
14+
15+
@IsString()
16+
@Validate(UniqueNicknameValidator)
17+
readonly nickname: string;
18+
}

src/user/dto/keyword.dto.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { IsString } from 'class-validator';
2+
3+
export class KeywordDto {
4+
@IsString()
5+
keyword: string;
6+
}

src/user/dto/update-user.dto.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { CreateUserDto } from './create-user.dto';
2+
import { PartialType, PickType } from '@nestjs/mapped-types';
3+
import { IsOptional, IsString, Validate } from 'class-validator';
4+
import { UniqueNicknameValidator } from '../user.validator';
5+
6+
export class UpdateUserDto {
7+
@IsOptional()
8+
@Validate(UniqueNicknameValidator)
9+
@IsString()
10+
nickname: string;
11+
12+
@IsOptional()
13+
@IsString()
14+
password: string;
15+
}

src/user/user.controller.spec.ts

-18
This file was deleted.

0 commit comments

Comments
 (0)