블로그 이미지
윤영식
Full Stacker, Application Architecter, KnowHow Dispenser and Bike Rider

Publication

Category

Recent Post

2021. 9. 30. 19:48 React/Architecture

React로 Login 화면을 개발한다. 

 

 

Gateway의 api server와 web dev server의 연결

Gateway를 개발환경에서 api server를 구동하고, web 화면 테스트를 위하여 web dev server가 구동하면 web dev server의 요청이 api server로 proxy 되어야 한다. 

  • apps/gateway/web/proxy.conf.json 파일을 생성한다.
  • apps/gateway/web/project.json에 "proxyConfig" 위치와 "port"는 7000 번으로 설정한다.
// project.json 일부분 

"serve": {
      "executor": "@nrwl/web:dev-server",
      "options": {
        "buildTarget": "gateway-web:build",
        "hmr": true,
        "proxyConfig": "apps/gateway/web/proxy.conf.json", <== 요기
        "port": 7000  <== 요기
      },
      ...
}

proxy.conf.json 내역

{
  "/gateway/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/api/gateway/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/api/auth/login": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/dashboard/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/dashboard/api/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/configuration/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/configuration/api/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/back-office/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  },
  "/back-office/api/*": {
    "target": "http://localhost:8000",
    "secure": false,
    "changeOrigin": true,
    "ws": true
  }
}

 

 

Web 패키지들 설치

rnn-stack의 글을 참조한다.

  • antd components 패키지 설치: "yarn add antd @ant-design/icons"
  • React Router 설치: "yarn add react-router react-router-dom"
  • Axios 설치: "yarn add axios"

VSCode의 디버깅 실행 환경파일인 launch.json파일에 Web Dev Server를 수행할 수 있도록 설정한다. VSCode에서 실행보다는 별도 terminal 에서 수행하는 것을 권장하고, 옵션으로 사용한다.

// launch.json 일부 내역 
    {
      "type": "node",
      "request": "launch",
      "name": "Gateway API",
      "program": "${workspaceFolder}/apps/gateway/api/src/main.ts",
      "preLaunchTask": "build:gateway-api",
      "outFiles": ["${workspaceFolder}/dist/apps/gateway/api/**/*.js"]
    },
    // 하기 내역
    {
      "type": "node",
      "request": "launch",
      "name": "Gatewy Web",
      "runtimeExecutable": "npm",
      "runtimeArgs": ["run", "start:gateway-web"],
      "outFiles": ["${workspaceFolder}/dist/apps/gateway/web/**/*.js"]
    },

npm의 script로 "start:gateway-web"을 설정한다.

// package.json 일부 

  "scripts": {
    "build:gateway-api": "nx build gateway-api",
    "start:gateway-api": "nx serve gateway-api",
    "build:gateway-web": "nx build gateway-web",
    "start:gateway-web": "nx serve gateway-web",
    ....
  },

이제 VS Code에서 Gateway web을 실행하고, http://localhost:7000/ 호출한다.

Gateway Web Dev Server 기동

 

 

Login 화면 저작

nx 명령을 이용해 login 컴포넌트를 생성한다.

$> nx g @nrwl/react:component login --project=gateway-web

생성된 파일들

login.tsx 내역

// login.tsx
import { Row, Col, Form, Input, Button } from 'antd';
import { httpService } from '@rnm/ui';
import { LoginDto } from '@rnm/model';
import styles from './login.module.scss';

export function Login() {
  const onFinish = (user: LoginDto) => {
    httpService.post<LoginDto>('/api/auth/login', user).subscribe((result: LoginDto) => {
      console.log('Success:', result);
      // redirect dashboard
      location.href = '/dashboard';
    });
  };

  const onFinishFailed = (errorInfo: any) => {
    console.log('Failed:', errorInfo);
  };

  return (
    <div className={styles.login_container}>
      <div className={styles.center_bg}>
        <Row justify="center" align="middle" className={styles.form_container}>
          <Col span={16} offset={6}>
            <Form
              name="basic"
              layout="vertical"
              labelCol={{ span: 16 }}
              wrapperCol={{ span: 16 }}
              initialValues={{ remember: true }}
              onFinish={onFinish}
              onFinishFailed={onFinishFailed}
              autoComplete="off"
            >
              <Form.Item
                label="Username"
                name="username"
                rules={[{ required: true, message: 'Please input your username!' }]}
              >
                <Input />
              </Form.Item>

              <Form.Item
                label="Password"
                name="password"
                rules={[{ required: true, message: 'Please input your password!' }]}
              >
                <Input.Password />
              </Form.Item>

              <Form.Item wrapperCol={{ span: 16 }}>
                <Button type="primary" htmlType="submit" block>
                  Submit
                </Button>
              </Form.Item>
            </Form>
          </Col>
        </Row>
      </div>
    </div>
  );
}

export default Login;

 

 

Gateway API 서버 호출하기

Web 에서 사용할 라이브러리는 분리하고, API와 Web이 공용하는 부분은 Model에만 국한한다. 따라서 @rnm/model 패키지를 생성한다. 

$> nx g @nrwl/react:lib model --publishable --importPath=@rnm/model

domain/entities/user/user.model.ts 을 libs/model/src/lib/user/user.model.ts 로 copy하고, domain에서는 삭제한다. 그리고 user.model사용하는 클래스를 일괄 수정한다. 

// user.model.ts
export interface User {
  id?: number;
  username: string;
  password?: string;
  email?: string;
  firstName?: string;
  lastName?: string;
  role?: string;
  sub?: string | number;
  currentHashedRefreshToken?: string;
}
export type LoginDto = Pick<User, 'username' | 'password'>;
export type TokenPayload = Omit<User, 'password'>;

export enum UserRole {
  ADMIN = 'ADMIN',
  MANAGER = 'MANAGER',
  CUSTOMER = 'CUSTOMER',
  GUEST = 'GUEST',
}

 

다음으로 libs/ui/src/lib/ajax/http.service.ts 파일을 생성하고, http.service.ts 코드에, error처리 notification을 추가한다.

// http.service.ts 일부
  import { notification } from 'antd';
  
  ...
  private executeRequest<T>(args: RequestArgs): Observable<T> {
    const { method, url, queryParams, payload } = args;
    let request: AxiosPromise<T>;
    switch (method) {
      case HttpMethod.GET:
        request = this.httpClient.get<T>(url, { params: queryParams });
        break;
      case HttpMethod.POST:
        request = this.httpClient.post<T>(url, payload);
        break;
      case HttpMethod.PUT:
        request = this.httpClient.put<T>(url, payload);
        break;
      case HttpMethod.PATCH:
        request = this.httpClient.patch<T>(url, payload);
        break;
      case HttpMethod.DELETE:
        request = this.httpClient.delete<T>(url);
        break;
    }

    return new Observable<T>((observer: Observer<T>) => {
      request
        .then((response: AxiosResponse) => {
          observer.next(response.data);
        })
        .catch((error: AxiosError | Error) => {
          this.abort(true);
          if (axios.isAxiosError(error)) {
            if (error.response) {
              // NestJS의 global-exception.filter.ts의 포멧
              const data: any = error.response?.data || {};
              this.showNotification(`[${data.statusCode}] ${data.error}`, data.message);
              console.log(`[${data.statusCode}] ${data.error}: ${data.message}`);
            }
          } else {
            this.showNotification('Unknow Error', error.message);
            console.log(error.message);
          }
        })
        .finally(() => {
          this.completed = true;
          observer.complete();
        });
      return () => this.abort();
    });
  }

  private showNotification(message: string, description: string): void {
    notification.error({
      message,
      description
    });
  }
}

에러처리는 libs/shared/src/lib/filter/global-exception.filter.ts 의 에러 포멧을 따른다. 

import { Request, Response } from 'express';
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger } from '@nestjs/common';

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const message = (exception as any).message;

    Logger.error(message, (exception as any).stack, `${request.method} ${request.url}`);

    const name = exception?.constructor?.name || 'HttpException';
    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    switch (name) {
      case 'HttpException':
        status = (exception as HttpException).getStatus();
        break;
      case 'UnauthorizedException':
        status = HttpStatus.UNAUTHORIZED;
        break;
      case 'ForbiddenException':
        status = HttpStatus.FORBIDDEN;
        break;
      case 'QueryFailedError':  // this is a TypeOrm error
        status = HttpStatus.UNPROCESSABLE_ENTITY;
        break;
      case 'EntityNotFoundError':  // this is another TypeOrm error
        status = HttpStatus.UNPROCESSABLE_ENTITY;
        break;
      case 'CannotCreateEntityIdMapError': // and another
        status = HttpStatus.UNPROCESSABLE_ENTITY;
        break;
      default:
        status = HttpStatus.INTERNAL_SERVER_ERROR;
    }

	// 에러 리턴 포멧
    response.status(status).json(
      {
        statusCode: status,
        error: name,
        message,
        method: request.method,
        path: request.url,
        timestamp: new Date().toISOString()
      }
    );
  }
}

 

테스트 진행시 UI가 Nest쪽 패키지를 사용하면 번들링 오류가 발생할 수 있다. 따라서 libs 하위의 패키지들은 향후 API용, WEB용 구분하여 사용하고, model 패키지만 공용으로 사용한다. API용, WEB용을 구분한다면 하기와 같이 별도 폴더로 묶어 관리하는게 좋아 보인다. 

api, web, model 분리

 

Nx 기반 library 생성 명령은 다음과 같다. 

// api library
$>  nx g @nrwl/nest:lib api/shared --publishable --importPath=@rnm/api-shared
$>  nx g @nrwl/nest:lib api/domain --publishable --importPath=@rnm/api-domain

// web library
$> nx g @nrwl/react:lib web/shared --publishable --importPath=@gv/web-shared
$> nx g @nrwl/react:lib web/domain --publishable --importPath=@gv/web-domain
$> nx g @nrwl/react:lib web/ui --publishable --importPath=@gv/web-ui

// model library
$> nx g @nrwl/nest:lib model --publishable --importPath=@gv/model

라이브러 생성 폴더 구조

 

소스: https://github.com/ysyun/rnm-stack/releases/tag/ms-9

 

Release ms-9 · ysyun/rnm-stack

[ms-9] added login component and enhanced ajax error notification

github.com

 

 

<참조>

- React 라이브러리 환경 구성

https://mobicon.tistory.com/580

 

[React HH-3] 라이브러리 설정 - Axios, RxJS

React 외에 애플리케이션 개발을 위한 라이브러리를 설치한다. UI Components PrimeReact, EUI, MaterialUI, AntD 검토후 소스레벨 최신으로 반영하고 있고, 다양한 비즈니스 UX 대응 가능한 AntD를 선택한다. //..

mobicon.tistory.com

- location.href와 location.replace 차이점

https://opentutorials.org/module/2919/22904

 

location.href 와 location.replace 차이점 - JavaScript Tips

[출처] [자바스크립트] location.href 와 location.replace 의 차이점.|작성자 왕따짱 location.href location.replace   기능 새로운 페이지로 이동된다. 기존페이지를 새로운 페이지로 변경시킨다.   형태 속

opentutorials.org

 

posted by 윤영식
2021. 9. 30. 13:22 React/Architecture

 

NestJS에서 제공하는 Auth와 Role 기능을 확장해 본다. NestJS는 그외 Configuration, Logging, Filter, Interceptor등 다양한 기능을 확장하여 적용할 수 있도록 한다. 

 

 

Role 데코레이터 추가

Role 체크를 위한 데코레이터를 libs/shared/src/lib/decorator/roles.decorator.ts 를 추가한다.

// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

 

 

Role 가드 추가

request시에 user 정보의 role을 통해 match되는지를 체크하는 가드(guard)를 libs/shared/src/lib/guard/role.guard.ts 추가한다. 

  • 요구하는 roles가 없으면 bypass 한다.
  • user가 없다면 즉, 로그인한 사용자가 아니거나, Login Token이 없다면 Forbidden 에러를 발생시킨다.
// role.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

const matchRoles = (roles: string[], userRoles: string) => {
  return roles.some(role => role === userRoles);
};

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) { }

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles) {
      return true;
    }

    const req = context.switchToHttp().getRequest() as any;
    const user = req.user;
    if (!user) {
      throw new ForbiddenException('User does not exist');
    }
    return matchRoles(requiredRoles, user.role);
  }
}

 

로그인후 express의 request에 user 객체 할당

로그인을 하면 사용자 정보가 Token에 담긴다. @Role 데코레이터를 체크하기 전에 Token 정보를 기반으로 user 정보를 추출한다. 

  • 로그인 토큰: LOGIN_TOKEN

libs/domain/src/lib/auth/auth.middleware.ts 파일을 생성하고, 쿠키의 LOGIN_TOKEN에서 user정보를 얻는다.

import { ForbiddenException, Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { verify } from 'jsonwebtoken';

import { loadConfigJson } from '@rnm/shared';
const config: any = loadConfigJson();

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    if (req.user) {
      next();
      return;
    }
    
    const accessToken = req?.cookies?.LOGIN_TOKEN;
    let user;
    try {
      user = verify(
        accessToken,
        config?.AUTH?.SECRET,
      );
    } catch (error) {
      throw new ForbiddenException('Please register or sign in.');
    }

    if (user) {
      req.user = user;
    }
    next();
  }
}

request에 user를 할당하는 미들웨어와 Role Guard를 apps/gateway/api/src/app/app.module.ts 에 설정한다. 

  • RolesGuard 등록
  • AuthMiddleware path들 등록
// app.module.ts
import { join } from 'path';
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { APP_FILTER, APP_GUARD } from '@nestjs/core';
import { ServeStaticModule } from '@nestjs/serve-static';
import { TypeOrmModule } from '@nestjs/typeorm';
import { getMetadataArgsStorage } from 'typeorm';

import { GatewayApiAppService, EntitiesModule, AuthModule, AuthMiddleware } from '@rnm/domain';
import { GlobalExceptionFilter, ormConfigService, RolesGuard } from '@rnm/shared';

import { DashboardModule } from './dashboard/microservice/dashboard.module';
import { ConfigurationModule } from './configuration/microservice/configuration.module';
import { BackOfficeModule } from './back-office/microservice/back-office.module';
import { AppController } from './app.controller';
import { AuthController } from './auth/auth.controller';
import { UserController } from './user/user.controller';

@Module({
  imports: [
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, 'public'),
      exclude: [
        '/api/auth*',
        '/api/gateway*', '/api/dashboard*', '/api/configuration*', '/api/back-office*',
        '/dashboard*', '/configuration*', '/back-office*'
      ],
    }),
    // ORM
    TypeOrmModule.forRoot({
      ...ormConfigService.getTypeOrmConfig(),
      entities: getMetadataArgsStorage().tables.map(tbl => tbl.target)
    }),
    EntitiesModule,
    // MicroService
    DashboardModule,
    ConfigurationModule,
    BackOfficeModule,
    // Auth
    AuthModule
  ],
  controllers: [
    AuthController,
    AppController,
    UserController
  ],
  providers: [
    GatewayApiAppService,
    // Global Exception Filter
    {
      provide: APP_FILTER,
      useClass: GlobalExceptionFilter,
    },
    // 1) Role Guard 등록
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ]
})
export class AppModule implements NestModule {
  // 2) Auth Middleware 등록
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(AuthMiddleware)
      .forRoutes(...[
        { path: '/dashboard*', method: RequestMethod.ALL },
        { path: '/configuration*', method: RequestMethod.ALL },
        { path: '/back-office*', method: RequestMethod.ALL },
        { path: '/api*', method: RequestMethod.ALL },
      ]);
  }
}

 

 

Role 사용하기

user 테이블에 Role이 저장되어있다. 

user.model.ts 소스에 UserRole enum을 추가한다. 

// user.model.ts
export interface User {
  id?: number;
  username: string;
  password?: string;
  email?: string;
  firstName?: string;
  lastName?: string;
  role?: string;
  sub?: string | number;
  currentHashedRefreshToken?: string;
}
export type LoginDto = Pick<User, 'username' | 'password'>;
export type TokenPayload = Omit<User, 'password'>;

// User Role
export enum UserRole {
  ADMIN = 'ADMIN',
  MANAGER = 'MANAGER',
  CUSTOMER = 'CUSTOMER',
  GUEST = 'GUEST',
}

apps/gateway/api/src/app/user/user.controller.ts 안에 @Roles을 적용한다. 

// user.controller.ts
import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';

import { JwtAuthGuard, UserService } from '@rnm/domain';
import { User, UserRole } from '@rnm/model';
import { Roles } from '@rnm/shared';

@Controller('api/gateway/user')
export class UserController {
  constructor(
    private readonly service: UserService
  ) { }

  @UseGuards(JwtAuthGuard)
  @Post()
  @Roles(UserRole.ADMIN, UserRole.MANAGER) // <== 요기
  async create(@Body() data: User): Promise<User> {
    const savedUser = await this.service.create(data);
    if (!savedUser) {
      return;
    }
    return savedUser;
  }
  ....
 }

 

이후 열심히 사용해 보자.

소스: https://github.com/ysyun/rnm-stack/releases/tag/ms-8

 

Release ms-8 · ysyun/rnm-stack

[ms-8] added role guard for authorization

github.com

 

 

<참조>

- NestJS Authorization: https://docs.nestjs.kr/security/authorization

 

네스트JS 한국어 매뉴얼 사이트

네스트JS 한국, 네스트JS Korea 한국어 매뉴얼

docs.nestjs.kr

- JWT Role based authentication: https://github.com/rangle/jwt-role-based-authentication-examples

 

GitHub - rangle/jwt-role-based-authentication-examples: Implement the same backend using graphql, nestjs and deno.

Implement the same backend using graphql, nestjs and deno. - GitHub - rangle/jwt-role-based-authentication-examples: Implement the same backend using graphql, nestjs and deno.

github.com

 

posted by 윤영식
2021. 9. 30. 13:22 React/Architecture

Login Auth Token이 만료되었을 때 Refresh Token을 통하여 다시 Auth Token을 생성토록한다. 

  • Refresh Token을 서버에 저장한다. 다른 기기에서 로그인하면 기존 로그인 기기의 Refresh Token과 비교하여 틀리므로 여러 기기의 로그인을 방지한다. 
  • 서버에 여러개의 Refresh Token을 저장할 수 있다면 기기 제한을 할 수 있겠다. 또한 변조된 refresh token의 사용을 막을 수 있다.

 

User Entity 업데이트

libs/domain/src/lib/entities/user/user.entity.ts 에 refreshToken 컬럼을 추가한다. 

import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';

@Entity('user_iot')
export class UserEntity {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column({ unique: true, length: 50 })
  username!: string;

  @Column()
  password!: string;

  @Column({ length: 255 })
  email!: string;

  @Column({ name: 'first_name', length: 100 })
  firstName!: string;

  @Column({ name: 'last_name', length: 100 })
  lastName!: string;

  @Column({ default: 'GUEST' })
  role!: string;

  @CreateDateColumn({ name: 'created_at', select: false })
  createdAt?: Date;

  @CreateDateColumn({ name: 'updated_at', select: false })
  updatedAt?: Date;

  // refresh token 저장
  @Column({
    name: 'current_hashed_refresh_token',
    nullable: true
  })
  currentHashedRefreshToken?: string;

}

 

 

User Service에 refreshToken 매칭

Cookie의 REFRESH_TOKEN이 서버에 저장된 값과 맞으면 해당 user정보를 반환하는 코드를 libs/domain/src/lib/entities/user/user.service.ts 에 추가한다.

// user.service.ts 일부

  async getUserIfRefreshTokenMatches(refreshToken: string, id: number): Promise<User | undefined> {
    const user = await this.findOneById(id);

    const isRefreshTokenMatching = await bcryptCompare(
      refreshToken,
      user.currentHashedRefreshToken as string
    );

    if (isRefreshTokenMatching) {
      return user;
    }
    return;
  }

 

 

JWT Refresh Strategy와 Guard 추가

Guard에서 사용할 Refresh Strategy를 libs/domain/src/lib/auth/strategies/jwt-refresh.strategy.ts 파일 생성후 추가한다.

import { Request } from 'express';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';

import { UserService } from '@rnm/domain';
import { loadConfigJson } from '@rnm/shared';
import { TokenPayload, User } from '@rnm/model';

const config: any = loadConfigJson();

@Injectable()
export class JwtRefreshTokenStrategy extends PassportStrategy(Strategy, 'jwt-refresh-token') {
  constructor(
    private readonly userService: UserService,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromExtractors([(request: Request) => {
        return request?.cookies?.REFRESH_LOGIN_TOKEN;
      }]),
      secretOrKey: config?.AUTH?.REFRESH_SECRET,
      passReqToCallback: true,
    });
  }

  async validate(request: Request, payload: TokenPayload): Promise<User | undefined> {
    const refreshToken = request.cookies?.REFRESH_LOGIN_TOKEN;
    return this.userService.getUserIfRefreshTokenMatches(refreshToken, payload.id as number);
  }
}

Refresh Guard도 libs/domain/src/lib/auth/guards/jwt-auth-refresh.guard.ts 파일 생성하고 추가한다. 

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtRefreshGuard extends AuthGuard('jwt-refresh-token') { }

파일 추가후에는 항시 libs/domain/src/index.ts 안에 export를 해야 한다. 

export * from './lib/constants/core.contant';
export * from './lib/entities/user/user.entity';
export * from './lib/entities/user/user.service';
export * from './lib/entities/entity.module';

export * from './lib/models/request.model';

export * from './lib/auth/auth.service';
export * from './lib/auth/auth.middleware';
export * from './lib/auth/auth.module';
export * from './lib/auth/guards/local-auth.guard';
export * from './lib/auth/guards/jwt-auth.guard';
export * from './lib/auth/guards/jwt-auth-refresh.guard'; // <== 요기

export * from './lib/auth/strategies/local.strategy';
export * from './lib/auth/strategies/jwt.strategy';
export * from './lib/auth/strategies/jwt-refresh.strategy'; // <== 요기

export * from './lib/service/gateway/api/service/gateway-api-app.service';
export * from './lib/service/dashboard/api/service/dashboard-api-app.service';
export * from './lib/configuration/api/service/configuration-api-app.service';
export * from './lib/service/back-office/api/service/backoffice-api-app.service';

 

 

RefreshToken과 AuthToken을 Cookie에 실어 보내기

두가지 Token을 response cookie에 실어 보내기위해 먼저 cookie 생성하는 코드를  libs/domain/src/lib/auth/auth.service.ts 에 추가한다. 

// auth.service.ts 일부
  getCookieWithJwtAccessToken(payload: TokenPayload, hasAuthorization = false) {
    const token = this.jwtService.sign(payload, {
      secret: config?.AUTH?.SECRET || 'iot_app',
      expiresIn: config?.AUTH?.EXPIRED_ON || '1d'
    });
    if (hasAuthorization) {
      return [`LOGIN_TOKEN=${token}; HttpOnly; Path=/; Max-Age=${config?.AUTH?.EXPIRED_ON}`, `Authorization=${token}; HttpOnly; Path=/; Max-Age=${config?.AUTH?.EXPIRED_ON}`];
    } else {
      return [`LOGIN_TOKEN=${token}; HttpOnly; Path=/; Max-Age=${config?.AUTH?.EXPIRED_ON}`];
    }
  }

  getCookieWithJwtRefreshToken(payload: TokenPayload) {
    const token = this.jwtService.sign(payload, {
      secret: config?.AUTH?.REFRESH_SECRET || 'io_app_refresh',
      expiresIn: config?.AUTH?.REFRESH_EXPIRED_ON || '7d'
    });
    const cookie = `REFRESH_LOGIN_TOKEN=${token}; HttpOnly; Path=/; Max-Age=${config?.AUTH?.REFRESH_EXPIRED_ON}`;
    return {
      cookie,
      token
    }
  }

  getCookiesForLogOut() {
    return [
      'LOGIN_TOKEN=; HttpOnly; Path=/; Max-Age=0',
      'REFRESH_LOGIN_TOKEN=; HttpOnly; Path=/; Max-Age=0'
    ];
  }

로apps/gateway/api/src/app/auth/auth.controller.ts 안에 하기 로직을 추가한다.

  • 로그인 했을 때 해당 Cookie를 등록한다. 
  • 로그아웃할 때 해당 Cookie 내용을 삭제한다. 
  • Auth Token (Forbidden)오류 발생시 RefreshToken을 통해 Auth Token 재생성한다. 
import { Controller, Get, HttpCode, Post, Req, Res, UnauthorizedException, UseGuards } from '@nestjs/common';

import { AuthService, JwtAuthGuard, LocalAuthGuard, JwtRefreshGuard, RequestWithUser, UserService } from '@rnm/domain';
import { TokenPayload } from '@rnm/model';

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

  @UseGuards(LocalAuthGuard)
  @Post('login')
  async login(@Req() req: RequestWithUser): Promise<any> {
    const { user } = req;
    if (user) {
      const payload: TokenPayload = { username: user.username, sub: user.id, email: user.email, role: user.role };
      const accessTokenCookie = this.authService.getCookieWithJwtAccessToken(payload);
      const {
        cookie: refreshTokenCookie,
        token: refreshToken
      } = this.authService.getCookieWithJwtRefreshToken(payload);
      const loginUsernameCookie = this.authService.getCookieWithLoginUsername(payload);

      await this.userService.setCurrentRefreshToken(refreshToken, user.id);

      // 반드시 req.res로 쿠키를 설정
      req.res.setHeader('Set-Cookie', [...accessTokenCookie, refreshTokenCookie, loginUsernameCookie]);
      return {
        payload,
        accessTokenCookie,
        refreshTokenCookie
      };
    } else {
      throw new UnauthorizedException({
        error: 'User does not exist'
      });
    }
  }

  @UseGuards(JwtAuthGuard)
  @Post('logout')
  @HttpCode(200)
  async logout(@Req() req: RequestWithUser, @Res() res): Promise<any> {
    await this.userService.removeRefreshToken(req.user.id);
    res.setHeader('Set-Cookie', this.authService.getCookiesForLogOut());
  }

  @UseGuards(JwtAuthGuard)
  @Get()
  authenticate(@Req() req: RequestWithUser) {
    const user = req.user;
    return user;
  }

  // Refresh Guard를 적용한다.
  @UseGuards(JwtRefreshGuard)
  @Get('refresh')
  refresh(@Req() request: RequestWithUser) {
    const accessTokenCookie = this.authService.getCookieWithJwtAccessToken(request.user);

    request.res.setHeader('Set-Cookie', accessTokenCookie);
    return request.user;
  }
}

 

 

테스트하기 

Postman으로 테스트를 하면 accessTokenCookie가 나온다. 

로그인 결과값

accessTokenCookie를 복사하여 다른 명령 전송시에 Headers에 Cookie를 등록하여 사용한다. 

복사한 accessTokenCookie 사용

소스: https://github.com/ysyun/rnm-stack/releases/tag/ms-7

 

Release ms-7 · ysyun/rnm-stack

[ms-8] added role guard for authorization

github.com

 

 

<참조>

- Refresh Token 만들기
   소스: https://github.com/mwanago/nestjs-typescript

   문서: https://wanago.io/2020/09/21/api-nestjs-refresh-tokens-jwt/

 

API with NestJS #13. Implementing refresh tokens using JWT

It leaves quite a bit of room for improvement. In this article, we look into refresh tokens.

wanago.io

 

posted by 윤영식
2021. 9. 27. 18:04 React/Architecture

Micro Service의 앞단 Gateway에서 모든 호출에 대한 인증/인가를 처리한다. 먼저 JWT기반 인증에 대한 설정을 한다. 

  • passwort jwt 설정

 

JWT 처리를 위한  패키지 설치

passport를 통해 JWT를 관리한다.

  • userId/password 기반은 passport-local을 사용
  • JWT 체크 passport-jwt 사용
  • 추가적으로 http security를 위해 express middleware인 helmet 적용
$> yarn add @nestjs/jwt @nestjs/passport passport passport-local passport-jwt helmet
$> yarn add -D @types/passport-local @types/passport-jwt @types/express

 

Passport Local 적용

  • libs/domain/src/lib/auth/strategies 폴더 생성
  • local.strategy.ts 파일 생성
// local.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from '../auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException({
        error: 'Incorrect username and password'
      });
    }
    return user;
  }
}

 

Username/Password 체크하기

libs/domain/src/lib/auth/  폴더 생성하고, auth.service.ts 파일을 생성한다. 

  • validateUser: LocalStrategy에서 호출한다. username/password 로그인 유효성을 login 호출 이전에 체크한다.
  • login: validate user인 경우 사용자 정보를 통한 webtoken 생성
// auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

import { User } from '../entities/user/user.model';
import { UserService } from '../entities/user/user.service';
import { bcryptCompare } from '../utilties/bcrypt.util';

@Injectable()
export class AuthService {
  constructor(
    private readonly userService: UserService,
    private readonly jwtService: JwtService,
  ) { }

  async validateUser(username: string, pass: string): Promise<any> {
    const user: any = await this.userService.findOne(username) || {};
    if (user && pass === user.password) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  async login(loginUser: User): Promise<any> {
    const user = await this.userService.findOne(loginUser.username);
    if (user) {
      const payload = { username: user.username, sub: user.id, email: user.email, role: user.role };
      return {
        access_token: this.jwtService.sign(payload),
      };
    } else {
      throw new UnauthorizedException({
        error: 'There is no user'
      });
    }
  }
}

토큰 생성확인은 https://jwt.io/ 에서 할 수 있다. 

 

libs/domain/src/lib/auth/auth.module.ts 파일을 생성하고, config.json파일에 AUTH 프로퍼티를 추가한다. 

  • JwtModule을 등록한다. 
  • secret은 반드시 별도의 환경설정 파일에서 관리한다. 
  • AuthService도 등록한다.
  • User 정보를 read하기 위해 EntitiesModule도 imports 에 설정한다.
// apps/gateway/api/src/environments/config.json
{
  "HTTP_PORT": 8000,
  "AUTH": {
    "SECRET": "iot_secret_auth",
    "EXPIRED_ON": "1d"
  },
  ...
}

// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';

import { GatewayConfiguration, loadConfigJson } from '@rnm/shared';
import { EntitiesModule } from '../entities/entity.module';

import { AuthService } from './auth.service';
import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';

const config: GatewayConfiguration = loadConfigJson();

@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: config.AUTH?.SECRET,
      signOptions: { expiresIn: config.AUTH?.EXPIRED_ON },
    }),
    EntitiesModule
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule { }

 

Passport JWT 적용

로그인이 성공하면 jsonwebtoken 을 생성하고, 이후 request(요청)에 대해 JWT를 체크하는 환경설정을 한다.

  • libs/domain/src/lib/auth/strategies/jwt.strategy.ts 파일  생성
    • request header의 Bearer Token 체크 => Cookie 사용으로 변경 (master branch소스 참조)
    • 확인하는 secret 설정
  • AuthModule에 등록한다.
// jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Request } from 'express';

import { loadConfigJson } from '@rnm/shared';
const config: any = loadConfigJson();

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      // jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      // Cookie를 사용한다
      jwtFromRequest: ExtractJwt.fromExtractors([(request: Request) => {
        return request?.cookies?.LOGIN_TOKEN;
      }]),
      ignoreExpiration: false,
      secretOrKey: config?.AUTH?.SECRET
    });
  }

  async validate(user: any): Promise<any> {
    // return { id: payload.sub, username: payload.username };
    return user;
  }
}

 

Password 암호화

암호화 모듈 설치

$> yarn add bcrypt
$> yarn add -D @types/bcrypt

암호화 유틸리티를 생성한다. libs/domain/src/lib/utilties/bcrypt.util.ts 파일 생성

  • 사용자 생성시 패스워드
  • 입력 패스워드를 DB의 암호화된 패스워드와 비교한다.
import * as bcrypt from 'bcrypt';

// 사용자의 패스워드 암호화
export const bcryptHash = (plainText: string, saltOrRounds = 10): Promise<string> => {
  return bcrypt.hash(plainText, saltOrRounds);
}

// 입력 패스워드와 DB 패스워드 비교
export const bcryptCompare = (plainText: string, hashedMessage: string): Promise<boolean> => {
  return bcrypt.compare(plainText, hashedMessage);
}

libs/domain/src/lib/auth/auth.service.ts 의 validateUser에서 암호화된 password를 체크토록 수정한다.

// auth.service.ts
import { bcryptCompare } from '../utilties/bcrypt.util';

@Injectable()
export class AuthService {
  constructor(
    private readonly userService: UserService,
    private readonly jwtService: JwtService,
  ) { }

  async validateUser(username: string, pass: string): Promise<any> {
    const user: any = await this.userService.findOne(username) || {};
    // 암호화된 패스워드를 입력 패스워드와 같은지 비교
    const isMatch = await bcryptCompare(pass, user.password);
    if (user && isMatch) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
  ...
}

 

 

로그인 하기

  • apps/gateway/api/src/app/auth/auth.controller.ts 파일을 신규 생성.
  • apps/gateway/api/src/app/app.module.ts 설정
    • "auth/login" API에 대해 static server에 exclude 설정
    • AuthModule imports에 설정
    • AuthController 등록
// auth.controller.ts
import { Body, Controller, Post, UseGuards } from '@nestjs/common';

import { AuthService, LocalAuthGuard, User } from '@rnm/domain';

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

  @UseGuards(LocalAuthGuard)
  @Post('auth/login')
  async login(@Body() user: User): Promise<Response> {
    return this.authService.login(user);
  }
}

// app.module.ts
@Module({
  imports: [
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, 'public'),
      exclude: [
        '/auth/*',
        '/api/gateway*', '/api/dashboard*', '/api/configuration*', '/api/back-office*',
        '/dashboard*', '/configuration*', '/back-office*'
      ],
    }),
    // ORM
    TypeOrmModule.forRoot({
      ...ormConfigService.getTypeOrmConfig(),
      entities: getMetadataArgsStorage().tables.map(tbl => tbl.target)
    }),
    EntitiesModule,
    // MicroService
    DashboardModule,
    // Auth
    AuthModule
  ],
  controllers: [
    AppController,
    AuthController,
    UserController
  ],
  providers: [GatewayApiAppService]
})
export class AppModule { }

 

UseGuard에서 username/password는  LocalAuthGuard를 등록한다. 이를 위해 libs/domain/src/lib/auth/guards/local-auth.guard.ts 파일 생성한다.

import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';

import { JwtAuthGuard, User, UserService } from '@rnm/domain';

@Controller('api/gateway/user')
export class UserController {
  constructor(
    private readonly service: UserService
  ) { }

  @UseGuards(JwtAuthGuard)
  @Post()
  create(@Body() data: User): Promise<User> {
    return this.service.create(data);
  }

  @UseGuards(JwtAuthGuard)
  @Put(':id')
  updateOne(@Param('id') id: number, @Body() data: User): Promise<any> {
    return this.service.updateOne(id, data);
  }

  @UseGuards(JwtAuthGuard)
  @Get()
  findAll(): Promise<User[]> {
    return this.service.findAll();
  }

  @UseGuards(JwtAuthGuard)
  @Get(':username')
  findOne(@Param('username') username: string): Promise<User | undefined> {
    return this.service.findOne(username);
  }

  @UseGuards(JwtAuthGuard)
  @Delete(':id')
  deleteOne(@Param('id') id: string): Promise<any> {
    return this.service.deleteOne(id);
  }
}
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') { }

User 생성하는 apps/gateway/api/src/app/user/user.controller.ts 에도 @UseGuards 를 JWT 토큰 체크하는 Guard로 등록한다. 이를 위하여 libs/domain/src/lib/auth/guards/jwt-auth.guard.ts 파일을 생성한다. 

import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  canActivate(context: ExecutionContext) {
    // Add your custom authentication logic here
    // for example, call super.logIn(request) to establish a session.
    return super.canActivate(context);
  }

  handleRequest(err: any, user: any, info: any, context: any, status?: any) {
    // You can throw an exception based on either "info" or "err" arguments
    if (err || !user) {
      throw err || new UnauthorizedException();
    }
    return user;
  }
}

그리고 apps/gateway/api/src/app/user/user.controller.ts 에 @UseGuards를 "JwtAuthGuard"로 등록한다. 

import { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';

import { JwtAuthGuard, User, UserService } from '@rnm/domain';

@Controller('api/gateway/user')
export class UserController {
  constructor(
    private readonly service: UserService
  ) { }

  @UseGuards(JwtAuthGuard)
  @Post()
  create(@Body() data: User): Promise<User> {
    return this.service.create(data);
  }

  @UseGuards(JwtAuthGuard)
  @Put(':id')
  updateOne(@Param('id') id: number, @Body() data: User): Promise<any> {
    return this.service.updateOne(id, data);
  }

  @UseGuards(JwtAuthGuard)
  @Get()
  findAll(): Promise<User[]> {
    return this.service.findAll();
  }

  @UseGuards(JwtAuthGuard)
  @Get(':username')
  findOne(@Param('username') username: string): Promise<User | undefined> {
    return this.service.findOne(username);
  }

  @UseGuards(JwtAuthGuard)
  @Delete(':id')
  deleteOne(@Param('id') id: string): Promise<any> {
    return this.service.deleteOne(id);
  }
}

Postman으로 테스트 한다. 

소스: https://github.com/ysyun/rnm-stack/releases/tag/ms-6

 

Release ms-6 · ysyun/rnm-stack

[ms-6] add typeorm and jwt for auth

github.com

주의: 소스가 계속 업데이트되고 있기에 master branch를 참조해도 된다.

 

 

<참조>

- NestJS에 passport 기반 JWT 적용하기 

https://docs.nestjs.kr/security/authentication

 

네스트JS 한국어 매뉴얼 사이트

네스트JS 한국, 네스트JS Korea 한국어 매뉴얼

docs.nestjs.kr

- passport local 환경설정

http://www.passportjs.org/packages/passport-local/

 

passport-local

Local username and password authentication strategy for Passport.

www.passportjs.org

- passport-jwt 환경설정

https://www.passportjs.org/packages/passport-jwt/

 

passport-jwt

Passport authentication strategy using JSON Web Tokens

www.passportjs.org

- password 암호화

https://wanago.io/2020/05/25/api-nestjs-authenticating-users-bcrypt-passport-jwt-cookies/

 

API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies

1. API with NestJS #1. Controllers, routing and the module structure2. API with NestJS #2. Setting up a PostgreSQL database with TypeORM3. API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies4. API with NestJS #4. Error handling

wanago.io

posted by 윤영식
2021. 9. 27. 16:23 React/Architecture

Micro Service들이 멀티 Database를 사용할 경우 또는 Database Schema에 대한 주도권이 없으며 단지 연결하여 사용하는 입장의 Frontend Stack 개발자일 경우 Prisma보다는 TypeORM을 사용하는 것이 좋아보인다. 

 

 

TypeORM 설치 및 환경설정

nestjs 패키지와 typeorm 그리고 postgresql 패키지를 설치한다. 

$> yarn add @nestjs/typeorm typeorm pg

.env를 읽는 방식이 아니라 별도의 configuration json 파일에서 환경설정토록 한다. 

apps/gateway/api/src/environments/ 폴더에 orm-config.json 과 orm-config.prod.json 파일을 생성한다. 

  • synchronized는 반드시 개발시에만 true로 사용한다.
// orm-config.json
{
  "HOST": "localhost",
  "PORT": 5432,
  "USER": "iot",
  "PASSWORD": "1",
  "DATABASE": "rnm-stack",
  "ENTITIES": ["libs/domain/src/lib/entities/**/*.entity.ts"],
  "MODE": "dev",
  "SYNC": true
}

// Production 환경에서 사용
// orm-config.prod.json
{
  "HOST": "localhost",
  "PORT": 5432,
  "USER": "iot",
  "PASSWORD": "1",
  "DATABASE": "rnm-stack",
  "ENTITIES": ["libs/domain/src/lib/entities/**/*.entity.ts"],
  "MODE": "production",
  "SYNC": false
}

dev와 prod간의 config 스위칭을 위하여 apps/gateway/api/project.json 안에 replacement  문구를 추가한다. 

// project.json 일부내역
"fileReplacements": [
{
  "replace": "apps/gateway/api/src/environments/environment.ts",
  "with": "apps/gateway/api/src/environments/environment.prod.ts"
},
{
  "replace": "apps/gateway/api/src/environments/config.ts",
  "with": "apps/gateway/api/src/environments/config.prod.ts"
},
{
  "replace": "apps/gateway/api/src/environments/orm-config.ts",
  "with": "apps/gateway/api/src/environments/orm-config.prod.ts"
}
]

 

libs/shared/src/lib/configuration/ 폴더에 orm-config.service.ts 파일을 생성하고, orm-config.json 파일을 값을 다루도록 한다.

// orm-config.service.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { loadOrmConfiguration } from './config.service';

class OrmConfigService {
  constructor(private env: { [k: string]: any }) { }

  ensureValues(keys: string[]) {
    keys.forEach(k => this.getValue(k, true));
    return this;
  }

  getPort() {
    return this.getValue('PORT', true);
  }

  isProduction() {
    const mode = this.getValue('MODE', false);
    return mode !== 'dev';
  }

  getTypeOrmConfig(): TypeOrmModuleOptions {
    const config: TypeOrmModuleOptions = {
      type: 'postgres',
      host: this.getValue('HOST'),
      port: parseInt(this.getValue('PORT')),
      username: this.getValue('USER'),
      password: this.getValue('PASSWORD'),
      database: this.getValue('DATABASE'),
      entities: this.getValue('ENTITIES'),
      synchronize: this.getValue('SYNC'),
    };
    return config;
  }

  private getValue(key: string, throwOnMissing = true): any {
    const value = this.env[key];
    if (!value && throwOnMissing) {
      throw new Error(`config error - missing orm-config.${key}`);
    }
    return value;
  }

}

/**
 * Singleton Config
 */
const ormEnv: any = loadOrmConfiguration();
const ormConfigService = new OrmConfigService(ormEnv)
  .ensureValues([
    'HOST',
    'PORT',
    'USER',
    'PASSWORD',
    'DATABASE'
  ]);

export { ormConfigService };

apps/gateway/api/src/app/app.module.ts 에서 해당 configuration을 설정토록한다. @nestjs/typeorm의 모듈을 사용한다.

// app.module.ts
import { join } from 'path';
import { Module } from '@nestjs/common';
import { ServeStaticModule } from '@nestjs/serve-static';
import { TypeOrmModule } from '@nestjs/typeorm';
import { getMetadataArgsStorage } from 'typeorm';

import { GatewayApiAppService, EntitiesModule } from '@rnm/domain';
import { ormConfigService } from '@rnm/shared';

import { DashboardModule } from './dashboard/microservice/dashboard.module';
import { AppController } from './app.controller';
import { UserController } from './user/user.controller';

@Module({
  imports: [
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, 'public'),
      exclude: [
        '/api/gateway*', '/api/dashboard*', '/api/configuration*', '/api/back-office*',
        '/dashboard*', '/configuration*', '/back-office*'
      ],
    }),
    // ORM 환경을 설정한다. orm-config.json에 설정했던 entities의 내용을 등록한다. 
    TypeOrmModule.forRoot({
      ...ormConfigService.getTypeOrmConfig(),
      entities: getMetadataArgsStorage().tables.map(tbl => tbl.target)
    }),
    EntitiesModule,
    // MicroService
    DashboardModule,
  ],
  controllers: [
    AppController,
    UserController
  ],
  providers: [GatewayApiAppService]
})
export class AppModule { }

 

 

TypeORM사용 패턴

typeorm은 두가지 패턴을 선택적으로 사용할 수 있다.

  • Active Record: BeanEntity를 상속받아 entity내에서 CRUD 하기. (작은 서비스유리)
  • Data Mapper: Model은 별도이고, Respository가 DB와 연결하고, CRUD를 별도 서비스로 만든다. (큰 서비스유리)

Data Mapper 패턴을 사용하기 위해 libs/domain/src/lib/entities/user/ 폴더하위에 user.entity.ts, user.model, user.service.ts 파일을 생성한다. 

  • user.entity.ts: table schema
  • user.model.ts: interface DTO
  • user.service.ts: CRUD 
// user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity('user_iot')
export class UserEntity {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  username!: string;

  @Column()
  password!: string;

  @Column()
  email!: string;

  @Column()
  firstName!: string;

  @Column()
  lastName!: string;

  @Column({ default: false })
  isActive!: boolean;

  // USER, ADMIN, SUPER
  @Column({ default: 'USER' })
  role!: string;
}


// user.model.ts
export interface User {
  id: number;
  username: string;
  password: string;
  email?: string;
  firstName: string;
  lastName: string;
  isActive: boolean;
  role: string;
}


// user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { UserEntity } from './user.entity';
import { User } from './user.model';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(UserEntity) private repository: Repository<UserEntity>
  ) { }

  create(data: User): Promise<User> {
    return this.repository.save(data);
  }

  updateOne(id: number, data: User): Promise<any> {
    return this.repository.update(id, data);
  }

  findAll(): Promise<User[]> {
    return this.repository.find();
  }

  findOne(username: string): Promise<User | undefined> {
    // findOne이 객체라는 것에 주의
    return this.repository.findOne({ username });
  }

  deleteOne(id: string): Promise<any> {
    return this.repository.delete(id);
  }
}

libs/domain/src/lib/entities/  폴더에 entity.module.ts 생성하고, user.entity.ts를 등록한다.  entity.module.ts에는 user.entity외에 계속 추가되는 entity들을 forFeature로 등록한다. 

// entity.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserService } from './user/user.service';
import { UserEntity } from './user/user.entity';

@Module({
  imports: [
    TypeOrmModule.forFeature([UserEntity])
  ],
  providers: [UserService],
  exports: [UserService]
})
export class EntitiesModule { }

entity.module.ts을 사용하기 위해 apps/gateway/api/src/app/app.module.ts 파일에 등록한다. 

//app.module.ts 
import { TypeOrmModule } from '@nestjs/typeorm';
import { EntitiesModule } from '@rnm/domain';
import { ormConfigService } from '@rnm/shared';

@Module({
  imports: [
    ...
    // ORM
    TypeOrmModule.forRoot(ormConfigService.getTypeOrmConfig()),
    EntitiesModule,
    ...
  ],
  ...
})
export class AppModule { }

 

User CRUD 컨트롤러 작성 및 테스트

사용자 CRUD를 위한 controller를 작성한다. apps/gateway/api/src/app/user/ 폴더를 생성하고, user.controller.ts 파일을 생성한다. 

// user.controller.ts
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
import { User, UserService } from '@rnm/domain';

@Controller('api/gateway/user')
export class UserController {
  constructor(
    private readonly service: UserService
  ) { }

  @Post()
  create(@Body() data: User): Promise<User> {
    return this.service.create(data);
  }

  @Put(':id')
  updateOne(@Param('id') id: number, @Body() data: User): Promise<any> {
    return this.service.updateOne(id, data);
  }

  @Get()
  findAll(): Promise<User[]> {
    return this.service.findAll();
  }

  @Get(':username')
  findOne(@Param('username') username: string): Promise<User | undefined> {
    return this.service.findOne(username);
  }

  @Delete(':id')
  deleteOne(@Param('id') id: string): Promise<any> {
    return this.service.deleteOne(id);
  }
}

gateway를 start하면 dev모드에서 synchronized: true에서 "user_iot"  테이블이 자동으로 생성한다. 

Postman으로 호출을 해본다. 

  • POST method 선택
  • Body에 request json 입력
  • JSON 형식 선택
  • "Send" 클릭

 

 

<참조>

- TypeORM 사용형태

https://aonee.tistory.com/77

 

TypeORM 개념 및 설치 및 사용방법

👉 Typeorm 공식문서 ORM 개요 Object-relational mapping, 객체-관계 매핑 객체와 관계형 데이터베이스의 데이터를 자동으로 매핑(연결)해준다. 객체 지향 프로그래밍은 클래스를 사용하고, 관계형 데이

aonee.tistory.com

- TypeORM & Observable 로 변경 사용하는 방법

https://www.youtube.com/watch?v=Z6kw_aJHJLU 

- Nx에서 typeorm 로딩시 에러 이슈

https://github.com/nrwl/nx/issues/1393

 

tsconfig target incompatibility with NestJS and TypeORM module · Issue #1393 · nrwl/nx

Prerequisites I am running the latest version I checked the documentation and found no answer I checked to make sure that this issue has not already been filed I'm reporting the issue to the co...

github.com

 

posted by 윤영식