Skip to content

Commit

Permalink
Merge pull request #6 from PBTP/feat-authorazation
Browse files Browse the repository at this point in the history
DMVM-137 feat: 인증/인가
  • Loading branch information
shine-jung authored May 22, 2024
2 parents 4a486ff + eeba151 commit c5bec34
Show file tree
Hide file tree
Showing 23 changed files with 813 additions and 1,237 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ pids

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
*.pem
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,28 @@
},
"dependencies": {
"@aws-sdk/client-ssm": "^3.576.0",
"@liaoliaots/nestjs-redis": "^9.0.5",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.2",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.3.1",
"@nestjs/typeorm": "^10.0.2",
"aws-sdk": "^2.1623.0",
"axios": "^1.7.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"ioredis": "^5.4.1",
"nodemailer": "^6.9.13",
"npm": "^10.8.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.11.5",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"typeorm": "^0.3.20"
"typeorm": "^0.3.20",
"typeorm-naming-strategies": "^4.1.0"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
Expand Down
21 changes: 20 additions & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import { loadParameterStoreValue } from './env/ssm-config.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PreRegistrationServeyModule } from './pre-registration-servey/pre-registration-servey.module';
import { EmailModule } from './email/email.module';
import { CustomerModule } from './customer/customer.module';
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
import { AuthModule } from './auth/auth.module';
import { RedisModule } from '@liaoliaots/nestjs-redis';
import { CacheModule } from "./common/cache/cache.module";

@Module({
imports: [
Expand All @@ -16,7 +21,7 @@ import { EmailModule } from './email/email.module';
}),
TypeOrmModule.forRootAsync({
useFactory: async (configService: ConfigService) => {
const datasource = JSON.parse(configService.get('datasource'));
const datasource = JSON.parse(configService.get('datasource/db'));

return {
type: 'postgres',
Expand All @@ -27,12 +32,26 @@ import { EmailModule } from './email/email.module';
database: datasource.database,
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: false,
namingStrategy: new SnakeNamingStrategy(),
};
},
inject: [ConfigService],
}),
RedisModule.forRootAsync({
useFactory: async (configService: ConfigService) => {
const datasource = JSON.parse(configService.get('datasource/redis'));
return{
config: datasource,
readyLog: true
};
},
inject: [ConfigService],
}),
EmailModule,
PreRegistrationServeyModule,
CacheModule,
CustomerModule,
AuthModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
18 changes: 18 additions & 0 deletions src/auth/application/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';

describe('AuthService', () => {
let service: AuthService;

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

service = module.get<AuthService>(AuthService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
123 changes: 123 additions & 0 deletions src/auth/application/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Injectable } from '@nestjs/common';
import { AuthDto } from '../presentation/auth.dto';
import { CustomerService } from 'src/customer/application/customer.service';
import { Customer } from 'src/customer/entities/customer.entity';
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { CacheService } from '../../common/cache/cache.service';

@Injectable()
export class AuthService {
private readonly accessTokenOption: JwtSignOptions;
private readonly refreshTokenOption: JwtSignOptions;
private readonly accessTokenStrategy: string;

constructor(
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
private readonly cacheService: CacheService,
private readonly customerService: CustomerService,
) {
this.accessTokenOption = {
secret: this.configService.get<string>('jwt/access/secret'),
expiresIn: this.configService.get<number>('jwt/access/expire'),
};

this.refreshTokenOption = {
secret: this.configService.get<string>('jwt/refresh/secret'),
expiresIn: this.configService.get<number>('jwt/refresh/expire'),
};

this.accessTokenStrategy = this.configService.get<string>(
'jwt/access/strategy',
);
}

async login(dto: AuthDto): Promise<AuthDto> {
let customer: Customer = await this.customerService.findOne(dto);
customer = customer ?? (await this.customerService.create(dto));

const accessToken = this.jwtService.sign(
{ tokenType: 'access', subject: customer.customerId },
this.accessTokenOption,
);

await this.saveAccessToken(customer, accessToken);

const refreshToken = this.jwtService.sign(
{ tokenType: 'refresh', subject: customer.customerId },
this.refreshTokenOption,
);

await this.customerService.update({
...customer,
refreshToken: refreshToken,
});

return {
...customer,
accessToken: accessToken,
refreshToken: refreshToken,
};
}

async tokenRefresh(request: Request): Promise<AuthDto> {
const token = request.headers['authorization'].replace('Bearer ', '');

const payload = this.jwtService.decode(token);

const customer: Customer = await this.customerService.findOne({
customerId: payload.subject,
});

const accessToken = this.jwtService.sign(
{ tokenType: 'access', subject: customer.customerId },
this.accessTokenOption,
);

const refreshToken = this.jwtService.sign(
{ tokenType: 'refresh', subject: customer.customerId },
this.refreshTokenOption,
);

await this.saveAccessToken(customer, accessToken);

await this.customerService.update({
...customer,
refreshToken: refreshToken,
});

return {
...customer,
accessToken: accessToken,
refreshToken: refreshToken,
};
}

private async saveAccessToken(customer: Customer, accessToken: string) {
const key = `customer:${customer.customerId}:accessToken`;

if (this.accessTokenStrategy.toLowerCase() === 'unique') {
this.cacheService.get(key).then((v) => {
if (v) {
this.cacheService.del(v);
}
});

await this.cacheService.set(
key,
accessToken,
(this.accessTokenOption.expiresIn as number) / 1000,
);
}

await this.cacheService.set(
accessToken,
JSON.stringify({
...customer,
refreshToken: undefined,
}),
(this.accessTokenOption.expiresIn as number) / 1000,
);
}
}
37 changes: 37 additions & 0 deletions src/auth/application/jwt-access.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ConfigService } from '@nestjs/config';
import {
BadRequestException,
UnauthorizedException,
} from '@nestjs/common/exceptions';
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Customer } from '../../customer/entities/customer.entity';
import { CacheService } from '../../common/cache/cache.service';

@Injectable()
export class JwtAccessStrategy extends PassportStrategy(Strategy, 'access') {
constructor(
private readonly cacheService: CacheService,
private readonly configService: ConfigService,
) {
super({
secretOrKey: configService.get<string>('jwt/access/secret'),
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
passReqToCallback: true,
});
}

async validate(req: Request, payload: any): Promise<Customer> {
if (!payload) {
throw new UnauthorizedException();
}

if (payload.tokenType !== 'access') {
throw new BadRequestException();
}

const token = req.headers['authorization'].replace('Bearer ', '');
return JSON.parse(await this.cacheService.get(token)) as Customer;
}
}
37 changes: 37 additions & 0 deletions src/auth/application/jwt-refresh.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ConfigService } from '@nestjs/config';
import {
BadRequestException,
UnauthorizedException,
} from '@nestjs/common/exceptions';
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { CustomerService } from 'src/customer/application/customer.service';
import { Customer } from '../../customer/entities/customer.entity';

@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'refresh') {
constructor(
private readonly configService: ConfigService,
private readonly customerService: CustomerService,
) {
super({
secretOrKey: configService.get<string>('jwt/refresh/secret'),
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
passReqToCallback: true,
});
}

async validate(req: Request, payload: any): Promise<Customer> {
if (!payload) {
throw new UnauthorizedException();
}

if (payload.tokenType !== 'refresh') {
throw new BadRequestException();
}

const token = req.headers['authorization'].replace('Bearer ', '');
return await this.customerService.findOne({ refreshToken: token });
}
}
28 changes: 28 additions & 0 deletions 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 './application/auth.service';
import { AuthController } from './presentation/auth.controller';
import { CustomerModule } from 'src/customer/customer.module';
import { JwtModule } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { JwtAccessStrategy } from './application/jwt-access.strategy';
import { JwtRefreshStrategy } from './application/jwt-refresh.strategy';
import { PassportModule } from '@nestjs/passport';
import { CacheModule } from '../common/cache/cache.module';

@Module({
imports: [
JwtModule.registerAsync({
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('jwt/access/secret'),
}),
inject: [ConfigService],
}),
PassportModule.register({ defaultStrategy: 'access' }),
CacheModule,
CustomerModule,
],
controllers: [AuthController],
providers: [AuthService, JwtAccessStrategy, JwtRefreshStrategy],
exports: [AuthService, JwtAccessStrategy, JwtRefreshStrategy],
})
export class AuthModule {}
20 changes: 20 additions & 0 deletions src/auth/presentation/auth.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';
import { AuthService } from '../application/auth.service';

describe('AuthController', () => {
let controller: AuthController;

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

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

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
20 changes: 20 additions & 0 deletions src/auth/presentation/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common';
import { AuthService } from '../application/auth.service';
import { AuthDto } from './auth.dto';
import { AuthGuard } from '@nestjs/passport';

@Controller('/api/v1/auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@Post('/login')
async login(@Body() dto: AuthDto) {
return await this.authService.login(dto);
}

@Post('/refresh')
@UseGuards(AuthGuard('refresh'))
async refresh(@Req() req: Request) {
return await this.authService.tokenRefresh(req);
}
}
6 changes: 6 additions & 0 deletions src/auth/presentation/auth.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { CustomerDto } from 'src/customer/presentation/customer.dto';

export class AuthDto extends CustomerDto {
accessToken: string;
refreshToken: string;
}
10 changes: 10 additions & 0 deletions src/common/cache/cache.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from "@nestjs/common";
import { RedisModule } from "@liaoliaots/nestjs-redis";
import { CacheService } from "./cache.service";

@Module({
imports:[RedisModule],
providers: [CacheService],
exports: [CacheService],
})
export class CacheModule {}
Loading

0 comments on commit c5bec34

Please sign in to comment.