반응형

https://youtu.be/k7UtpY7uqDE

 

 

Application을 개발시 여러개의 DB에 접속하여 데이터를 핸들링 해야 하는 경우가 있습니다.
Nestjs에서는 TypeORM의 Multiple Connection을 이용하여 이러한 구현이 가능합니다.

예제에서는 다음과 같이 두개의 database를 사용합니다.

  • Database #1 : cat
  • Database #2 : slider

Table 정보

사용하는 테이블 생성 정보는 다음과 같습니다.

cat

CREATE TABLE `cat` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL COMMENT '고양이 이름',
  `age` int(11) NOT NULL COMMENT '고양이 나이',
  `breed` varchar(255) NOT NULL COMMENT '고양이 종류',
  PRIMARY KEY (`id`)
);

slider

CREATE TABLE `slider` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `image_url` varchar(200) NOT NULL,
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
);

 

프로젝트 생성

아래와 같이 프로젝트를 생성합니다.

nest new multi-connection

 

폴더로 이동 후 Visual Studio Code를 실행합니다.

cd multi-connection
code .

패키지 설치

아래와 같이 TypeORM 패키지를 설치합니다.

yarn add @nestjs/typeorm typeorm mysql2

 

첫번째 database 연결하기

ORM 설정파일

프로젝트에 orm 환경 설정 파일을 만듭니다.

  • 소스 폴더 : src/config/orm.config.ts
import { TypeOrmModuleOptions } from "@nestjs/typeorm";

function ormConfig(): TypeOrmModuleOptions {
    return {
        name: 'default',
        type: 'mysql',
        database: 'cat',
        host: 'localhost',
        port: Number(13306),
        username: 'cat',
        password: 'cat',
        logging: (process.env.NODE_ENV === 'local'),
        synchronize: true,
        entities: [__dirname + '/domain/*.entity.{ts,js}'],
        migrations: [__dirname + '/migrations/**/*.{ts,js}'],
        migrationsRun: false,
    };
}
function ormConfigSlider(): TypeOrmModuleOptions {
    return {
        name: 'sliderConnection',
        type: 'mysql',
        database: 'slider',
        host: 'localhost',
        port: Number(13306),
        username: 'slider',
        password: 'slider',
        logging: (process.env.NODE_ENV === 'local'),
        synchronize: true,
        entities: [__dirname + '/domain/*.entity.{ts,js}'],
        migrations: [__dirname + '/migrations/**/*.{ts,js}'],
        migrationsRun: true,
    };
}
export { ormConfig, ormConfigSlider };

 

orm config import 하기

app.module.ts 에 orm config를 import 합니다.

import { ormConfig } from './config/orm.config';

imports: [
    TypeOrmModule.forRootAsync({
      useFactory: ormConfig
    }),
    TypeOrmModule.forRootAsync({
      name: 'sliderConnection',
      useFactory: ormConfigSlider
    })
  ],

 

Entity 생성

typeorm-model-generator를 사용해서 테이블에 있는 cat, slider entity를 생성합니다.

사용법은 아래 포스트에서 확인하실 수 있습니다.
https://codegear.tistory.com/122

 

typeorm-model-generator 사용법

TypeORM 사용시 Entity 파일을 자동으로 생성해주는 tool이 있습니다.바로 typeorm-model-generator 입니다.이 tool을 사용하면 테이블을 참조하여 손쉽게 entity 파일을 만들 수 있습니다. typeorm-model-generator에

codegear.tistory.com

typeorm-model-generator -h localhost -d cat -p 13306 -u cat -x cat -e mysql -o ./gen_models
typeorm-model-generator -h localhost -d slider -p 13306 -u slider -x slider -e mysql -o ./gen_models

gen_models 폴더에 생성된 entity 파일을 domain 폴더로 복사합니다.

파일 이름을 아래와 같이 변경합니다.(ormConfig에서 설정한 파일명 형식)

Cat.ts -> Cat.entity.ts
Slider.ts -> Slider.entity.ts

Module에 Entity 선언하기

app.module.ts의 import 아래에 다음을 추가합니다.

(TypeOrmModule.forRootAsync 아래에 추가하면 됩니다)

TypeOrmModule.forFeature([Cat]),
TypeOrmModule.forFeature([Slider], 'sliderConnection')

Cat & Slider 데이터 조회하기

app.controller.ts에 다음을 추가합니다.

@Get('/cats')
async getCats(){
  return await this.appService.getCats();
}

@Get('/sliders')
async getSliders(){
  return await this.appService.getSliders();
}

app.service.ts에 다음을 추가합니다. (getCats, getSliders)

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Cat } from './domain/Cat.entity';
import { Slider } from './domain/Slider.entity';

@Injectable()
export class AppService {
  constructor(
    @InjectRepository(Cat)
    private catRepository: Repository<Cat>,
    @InjectRepository(Slider, 'sliderConnection')
    private sliderRepository: Repository<Slider>
  ){}
  
  getHello(): string {
    return 'Hello World!';
  }
  async getCats() {
    return await this.catRepository.find(); 
  }
  async getSliders() {
    return await this.sliderRepository.find();
  }
}

 

브라우저에서 다음을 각각 호출합니다.

http://localhost:3000/cats
http://localhost:3000/sliders

결과는 다음과 같습니다.

[
   {
      "id":1,
      "name":"kitty",
      "age":1,
      "breed":"cyprus"
   },
   {
      "id":2,
      "name":"pitty",
      "age":2,
      "breed":"percian"
   }
]
[
   {
      "id":1,
      "imageUrl":"https://..../images/test.jpg",
      "createdAt":"2024-08-19T14:08:20.000Z",
      "updatedAt":"2024-08-19T14:08:20.000Z"
   }
]

 

이상으로 Nestjs에서 TypeORM을 사용해서 여러개의 DB를 사용하는 방법에 대해 알아보았습니다.

반응형
반응형

다음은 이 글의 동영상 강좌입니다.

https://youtu.be/Dr4OXDLGsmI

Nestjs 프로젝트를 Docker화 하면 Application을 손쉽게 배포할 수 있습니다.

 

Docker가 설치된 서버에 docker로 빌드된 이미지만 업로드 하고 실행하면 됩니다.

github actions, aws pipeline등을 사용하면 모든 배포 단계를 자동화 할 수 있습니다.

이런 자동화를 통해 프로젝트의 생산성을 획기적으로 높일 수 있고,
개발자를 배포의 늪에서 해방 시켜 업무의 만족도를 높일 수 있습니다.

 

사전 설치 프로그램

우선 다음 프로그램을 설치합니다.

  • Nodejs
  • Docker
  • Nestjs를 사용하기 위해 nest cli를 설치합니다.
npm i -g @nestjs/cli

 

프로젝트 생성 및 실행

Nestjs 프로젝트를 생성합니다.

nest new nest-docker

 

프로젝트를 실행합니다.

npm start

 

Dockerfile / .dockerignore 파일 작성

프로젝트 root 폴더에 "Dockerfile" 파일을 생성합니다.

  • docker image 빌드시 사용하는 설계도입니다.
FROM node:18
RUN mkdir -p /var/app
WORKDIR /var/app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD [ "node", "dist/main.js" ]

프로젝트 root 폴더에 ".dockerignore" 파일을 생성합니다.

  • container에 불필요한 파일들이 복제되는 것을 막기 위한 용도입니다.
.git
*Dockerfile*
node_modules

 

Docker 이미지 생성

다음 명령으로 docker 이미지를 생성합니다.

docker build . -t nest-docker

 

Docker 실행

다음 명령으로 docker를 실행합니다.

docker container run -d -p 3000:3000 nest-docker

 

브라우저에서 확인해보기

브라우저에서 다음 url을 입력하여 확인합니다.

http://localhost:3000

다음과 같이 결과를 확인할 수 있습니다.

 

반응형
반응형

다음은 이 글의 동영상 강좌입니다.

https://youtu.be/yjhIxpR_-gE

연재 마지막 순서로 첫화면에 이미지 슬라이드를 같이 개발합니다.

 

우선 관리자에서 이미지를 5개 정도 업로드합니다.

이미지 슬라이드는 Bootstrap-vue의 Carousel 컴퍼넌트를 사용합니다. 

https://bootstrap-vue.org/docs/components/carousel

 

BootstrapVue

Quickly integrate Bootstrap v4 components with Vue.js

bootstrap-vue.org

Carousel 소스를 복사하여 다음과 같이 수정합니다.

pages/index.vue

<template>
  <div>
    <b-carousel
      id="carousel-1"
      v-model="slide"
      :interval="4000"
      controls
      indicators
      background="#ababab"
      img-width="1024"
      img-height="480"
      style="text-shadow: 1px 1px 2px #333;"
      @sliding-start="onSlideStart"
      @sliding-end="onSlideEnd"
    >
      <!-- Text slides with image -->
      <div v-for="slider in sliders" v-bind:key="slider.id">
        <b-carousel-slide
          caption="First slide"
          text="Nulla vitae elit libero, a pharetra augue mollis interdum."
          :img-src="slider.imageUrl"
        ></b-carousel-slide>
      </div>
      
    </b-carousel>

    <p class="mt-4">
      Slide #: {{ slide }}<br>
      Sliding: {{ sliding }}
    </p>
  </div>
</template>
<script>
export default {
  name: 'IndexPage',
  data() {
    return {
      sliders: '',
      slide: 0,
      sliding: null
    }
  },
  computed: {
    accessToken() {
      return this.$store.state.accessToken;
    }
  },
  mounted() {
    this.getSliders();
  },
  methods: {
    async getSliders(){
      const options = {
          headers: {
              Authorization: `Bearer ${this.accessToken}`
          }
      }
      const path = '/slider';
      await this.$axios.get(
        path,
        options
      ).then(response=> {
        this.sliders = response.data;
        console.log(this.sliders);
      }).catch(error => {
        console.log(error);
      })
    },
    onSlideStart(slide) {
      this.sliding = true
    },
    onSlideEnd(slide) {
      this.sliding = false
    }
  }
}
</script>

서버를 실행하면 다음과 같이 화면이 스크롤 됩니다.

이렇게 해서 이미지 슬라이드가 완성되었습니다.

 

Vue와 Nestjs를 공부하시는 분들에게 도움이 되었으면 좋겠습니다.

 

수고하셨습니다^^

반응형
반응형

다음은 이 글의 동영상 강의 입니다.

https://youtu.be/_bmxSSTzQsQ

지난 시간에 S3와 DB에 업로드한 파일을 조회하고 삭제하는 기능을 구현해 봅니다.


이미지 조회 기능 구현

slider-api 프로젝트에서 slider를 조회하는 api를 다음과 같이 추가합니다.

/src/slider/slider.controller.ts

@Get('')
async getSlider(): Promise<Slider[]> {
    return await this.sliderService.getSliders();
}

/src/slider/slider.service.ts

async getSliders(): Promise<Slider[]> {
    return await this.sliderRepository.find();
}

slider-front 프로젝트에 조회 기능을 추가합니다.

/pages/admin/index.vue

data()에 sliders 변수를 추가합니다.

data() {
    return {
        file1: null,
        bucketName: this.$env.S3_BUCKET,
        bucketRegion: this.$env.S3_REGION,
        identityPoolId: this.$env.S3_IDENTITY_POOL_ID,
        sliders: ''
    }
},

methods: 아래

async getSliders(){
    const options = {
        headers: {
            Authorization: `Bearer ${this.accessToken}`
        }
    }
    const path = '/slider';
    await this.$axios.get(
        path,
        options
    ).then(response => {
        this.sliders = response.data;
        console.log(this.sliders);
    }).catch(error => {
        console.log(error);
    })
},

이미지를 보여주기 위해서 bootstrap-vue의 card를 사용합니다.

https://bootstrap-vue.org/docs/components/card 

 

BootstrapVue

Quickly integrate Bootstrap v4 components with Vue.js

bootstrap-vue.org

card component 소스를 복사하여 다음과 같이 수정합니다.

/pages/admin/index.vue

<div>
    <b-row>
        <b-col>
            <div v-for="slider in sliders" v-bind:key="slider.id">
                <div>
                    <b-card
                        :img-src="slider.imageUrl"
                        img-alt="slider"
                        img-top
                        tag="article"
                        style="max-width: 20rem;"
                        class="mb-2"
                    >
                        <b-card-text>
                        </b-card-text>
                        <b-button variant="primary" @click="deleteSlider(slider.id)">삭제</b-button>
                    </b-card>
                    </div>
            </div>
        </b-col>
    </b-row>
</div>

조회 테스트를 합니다.

이미지가 보이지 않습니다.

원인은 이미지 파일이 잘못 올라갔기 때문이므로, 업로드 소스를 "Body: this.file1" 으로 수정합니다.

await s3.upload({
    ACL: 'public-read',
    Body: this.file1,
    Key: this.genDateTime()+'_'+this.file1.name
}

이미지를 다시 업로드 하면 다음과 같이 표시됩니다.

 

이미지 삭제 기능 구현

slider-api에 삭제 api를 추가합니다.

/src/slider/slider.controller.ts

@Delete('/:id')
async deleteSlider(@Param('id') id, @Request() requestAnimationFrame, @Response() res): Promise<any> {
    return await this.sliderService.deleteSlider(id);
}

/src/slider/slider.service.ts

import * as AWS from 'aws-sdk';

async getSlider(id: number): Promise<Slider> {
    const slider = await this.sliderRepository.findOne({where: {id}});
    if(slider){
        return slider;
    }else{
        throw new NotFoundException(`Slider with ID ${id} not found!`);
    }
}

async deleteSlider(id): Promise<any> {
    const slider = await this.getSlider(id);
    if(slider){
        const bucketName = process.env.S3_BUCKET;
        const region = process.env.S3_REGION;
        const IdentityPoolId = process.env.S3_IDENTITY_POOL_ID;
        AWS.config.update({
            region,
            credentials: new AWS.CognitoIdentityCredentials({
                IdentityPoolId
            })
        });
        const s3 = new AWS.S3({
            apiVersion: '2006-03-01',
            params: {
                Bucket: bucketName
            }
        });
        //s3 파일 삭제
        const uriName = decodeURI(slider.imageUrl);
        const fileNamePos = uriName.indexOf('/images/');
        if(fileNamePos === -1){
            throw new NotFoundException();
        }
        const fileName = uriName.substring(fileNamePos + 8);
        s3.deleteObject({
            Bucket: bucketName,
            Key: 'images/'+fileName
        }), (err) => {
            if(err) {
                console.log(err);
                throw new InternalServerErrorException(err);
            }
        }
        //table에서 삭제
        return await this.sliderRepository.delete(id);
    }
}

터미널에서 다음 명령어로 aws-sdk 패키지를 추가합니다.

yarn add aws-sdk

환경 설정 파일 .env에 다음 내용을 추가합니다.
.env

S3_BUCKET=codegear-slider-bucket
S3_REGION=ap-northeast-2
S3-IDENTITY_POOL_ID=ap-northeast-2:*****************************

slider-front 프로젝트에 delete method를 추가합니다.
/pages/admin/index.vue

<b-button variant="primary" @click="deleteSlider(slider.id)">삭제</b-button>
.....
async deleteSlider(id){
    const headers = {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${this.accessToken}`
    }
    await this.$axios.delete(
        `/slider/${id}`,
        {headers}
    ).then(async response =>{
        await this.getSliders();
    }).catch(error=> {
        console.log(error);
    })
}

삭제 버튼을 클릭하면 이미지가 삭제되는 것을 확인할 수 있습니다.

 

반응형
반응형

다음은 이 글의 동영상 강의 입니다.

https://youtu.be/g5LaZ1IPJXE

지난시간에 S3에 업로드가 완료된 파일을 이용해서 Backend API를 이용해 DB에 데이터를 저장하는 것을 진행합니다.

 

S3 파일명 유니크 하게 만들기

frontend 프로젝트에 moment 라는 패키지를 설치합니다.

yarn add moment

moment는 자바스크립트에서 날짜를 핸들링할때 유용한 패키지입니다.

vue에서 moment를 import 합니다.

/admin/index.vue

import moment from 'moment';

다음과 같이 현재 날짜와 시간을 문자열로 만들어주는 function을 methods: 아래에 추가합니다.

genDateTime() {
    return moment().format('YYYYMMDDHHmmss');
},

S3 파일 업로드 모듈을 다음과 같이 변경합니다.

await s3.upload({
    ACL: 'public-read',
    Body: this.genDateTime()+'_'+this.file1,
    Key: this.genDateTime()+'_'+this.file1.name
}

파일을 업로드 하면 다음과 같이 파일명이 생성됩니다.

 

Slider 테이블 생성

우선 MySQL Workbench를 이용해서 테이블을 생성합니다.

id: key값

image_url: S3의 url 정보

created_at: 생성일

updated_at: 수정일

 

Slider 저장용 Backend API 개발

slider-api 프로젝트에서 entity 파일을 다음과 같이 만듭니다.

/domain/slider.entity.ts

import {Column, Entity, PrimaryGeneratedColumn} from "typeorm";

@Entity('slider')
export class Slider {
    @PrimaryGeneratedColumn({type: 'int', name: 'id'})
    id: number;

    @Column('varchar', {name: 'image_url', length: 200})
    imageUrl: string;

    @Column('timestamp', {
        name: 'created_at',
        default: ()=> 'CURRENT_TIMESTAMP'
    })
    createAt: Date;

    @Column('timestamp', {
        name: 'updated_at',
        default: ()=> 'CURRENT_TIMESTAMP'
    })
    updatedAt: Date;
}

nest generator를 이용해서

slider module, controller, service를 추가합니다.

nest g module slider
nest g controller slider
nest g service slider

SliderModule에 entity를 추가합니다.

/slider/slider.module.ts

@Module({
  imports: [
    TypeOrmModule.forFeature([Slider])
  ],
  controllers: [SliderController],
  providers: [SliderService]
})

Controller에 slider api를 post로 추가합니다.

/slider/slider.controller.ts

import {BadRequestException, Body, Controller, Post, Response} from '@nestjs/common';
import {SliderService} from "./slider.service";

@Controller('slider')
export class SliderController {
    constructor(
        private sliderService: SliderService
    ) {}

    @Post('')
    async createSlider(
        @Body() createSliderDTO, @Response() res
    ): Promise<any>{
        const { imageUrl } = createSliderDTO.body;
        if(!imageUrl) throw new BadRequestException();
        await this.sliderService.createSlider({imageUrl});
        res.status(200).json({'message': 'OK'});
        return;
    }
}

Service에 createSlider method를 추가합니다.

/slider/slider.service.ts

import { Injectable } from '@nestjs/common';
import {InjectRepository} from "@nestjs/typeorm";
import {Slider} from "../domain/slider.entity";
import {Repository} from "typeorm";

@Injectable()
export class SliderService {
    constructor(
        @InjectRepository(Slider)
        private sliderRepository: Repository<Slider>
    ) {}
    async createSlider(param: { imageUrl: any }): Promise<any> {
        const {imageUrl} = param;
        await this.sliderRepository.save({imageUrl});
    }
}

 

Frontend 에서 slider API 호출하기

slider-front 프로젝트에서 s3 upload 후에 backend api를 호출하도록 소스를 변경합니다.

async saveImage(imageUrl){
    const headers = {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${this.accessToken}`
    }
    const body = {
        'imageUrl': imageUrl
    }
    await this.$axios.post(
        '/slider',
        {body},
        {headers}
    ).catch(error =>{
        console.log(error);
    })

},
async upload() {
    if(!this.file1){
        alert('File을 선택해주세요!')
    }else{
        console.log(this.file1);
        AWS.config.region = this.bucketRegion;
        AWS.config.credentials = new AWS.CognitoIdentityCredentials({
            IdentityPoolId: this.identityPoolId,
        });
        const s3 = new AWS.S3({
            apiVersion: '2006-03-01',
            params: {
                Bucket: this.bucketName+'/images'
            }
        });
        await s3.upload({
            ACL: 'public-read',
            Body: this.genDateTime()+'_'+this.file1,
            Key: this.genDateTime()+'_'+this.file1.name
        }, (error)=>{
            if(error){
                this.errorHandler(error);
            }
        }).promise().then((data)=>{
            console.log('File uploaded!!!')
            console.log(data);
            this.saveImage(data.Location);
        })
    }
}

파일 업로드 테스트를 합니다.

 

DB에 다음과 같이 데이터가 들어간것을 볼 수 있습니다.

이렇게 해서 S3 업로드 파일을 Backend API를 통해 DB에 저장하는 기능을 구현해 보았습니다.

반응형
반응형

Frontend에서 S3 버킷에 파일을 업로드 하는 것을 알아보겠습니다.

 

다음은 이 글의 동영상 강좌입니다.

https://youtu.be/7FsycVjRq8Q

 

slider-front 프로젝트를 열고 admin 페이지를 생성합니다.

내용은 index.vue를 복사하여 만듭니다.

/pages/admin/index.vue

<template>
  <div>
    <div>관리자 페이지</div>
  </div>
</template>
<script>
export default {
  name: 'IndexPage',
  computed: {
    accessToken() {
      return this.$store.state.accessToken
    }
  }
}
</script>

관리자 메뉴 링크 클릭시 위 페이지로 이동하도록 아래와 같이 변경합니다.

/layouts/default.vue

async admin() {
  await this.$router.replace('/admin/');
}

 

관리자 메뉴의 링크가 잘 작동하는지 테스트를 합니다.

 

dotenv와 aws-sdk 패키지를 추가합니다.

yarn add @nuxtjs/dotenv dotenv aws-sdk

aws 설정을 위해 plugins 폴더를 추가하고 env-var.js 파일을 아래와 같이 추가합니다.

export default ({
    app, context
}, inject) => {
    const env = {}
    env.S3_BUCKET = 'codegear-slider-bucket'; //버킷명 입력
    env.S3_REGION = 'ap-northeast-2'; //리전명 입력
    env.S3_IDENTITY_POOL_ID = '설정한 IDENTITY_POOL_ID값 입력';
    inject('env', env)
}

 

파일 업로드를 위해 Bootstrap-Vue의 Form File Component를 추가합니다.

https://bootstrap-vue.org/docs/components/form-file

 

BootstrapVue

Quickly integrate Bootstrap v4 components with Vue.js

bootstrap-vue.org

Admin 페이지를 다음과 같이 수정합니다.

/admin/index.vue

<template>
    <div>
        <b-form-file
            ref="file-input"
            v-model="file1"
            :state="Boolean(file1)"
            placeholder="Choose a file or drop it here..."
            drop-placeholder="Drop file here..."
            accept="image/*"
        ></b-form-file>
        <div class="mt-3">Selected file: {{ file1 ? file1.name : '' }}</div>
        <b-button @click="clearFiles" class="mr-2">Reset</b-button>
        <b-button @click="upload" class="mr-1">Upload</b-button>
    </div>
</template>

<script>
import AWS from 'aws-sdk';

export default {
    name: 'IndexPage',
    data() {
        return {
            file1: null,
            bucketName: this.$env.S3_BUCKET,
            bucketRegion: this.$env.S3_REGION,
            IdentityPoolId: this.$env.S3_IDENTITY_POOL_ID,
        }
    },
    computed: {
        accessToken() {
            return this.$store.state.accessToken;
        }
    },
    methods: {
        clearFiles() {
            this.$refs['file-input'].reset()
        },
        async upload() {
            if(!this.file1) {
                alert('File을 선택해주세요.')
            }else{
                console.log(this.file1);
                console.log(`${this.bucketRegion} ${this.bucketName}`);
                AWS.config.region = this.bucketRegion;
                AWS.config.credentials = new AWS.CognitoIdentityCredentials({
                    IdentityPoolId: this.IdentityPoolId,
                });
                const s3 = new AWS.S3({
                    apiVersion: '2006-03-01',
                    params: {
                        Bucket: this.bucketName+'/images'
                    }
                });
                await s3.upload({
                    ACL: 'public-read',
                    Body: this.file1,
                    Key: this.file1.name
                }, (error)=>{
                    if(error){
                        this.errorHandler(error);
                        // return alert('There was an error uploading your photo: ', error.message);
                    }
                }).promise().then((data)=>{
                    console.log('File uploaded!!!')
                    console.log(data);
                })
            };
        },
    }
}
</script>

파일 업로드 테스트를 해봅니다.

위와 같이 정상적으로 업로드가 안되고 AccessDenied가 나올 경우,
AWS의 IAM - 역할 메뉴에서 다음 항목을 검색합니다.

Cognito_SliderS3AccessUnauth_Role

해당항목을 클릭하고 들어가면,
아래와 같은 정책 이름이 보입니다.

"oneClick_Cognito_SliderS3AccessUnauth_Role_..." 항목을 클릭합니다.

JSON을 선택하여 아래와 같이 작성 후 정책 검토를 한 후 저장합니다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": [
                "arn:aws:s3:::codegear-slider-bucket/*",
                "arn:aws:s3:::codegear-slider-bucket"
            ]
        }
    ]
}

 

 

반응형
반응형

다음은 이 글의 동영상 강좌입니다.

https://youtu.be/A8FcYGEe2i4

이번 시간은 아마존에서 제공하는 Storage Service인 S3에 파일을 업로드 하는 것을 알아봅니다.

S3는 Simple Storage Service의 각 단어 첫번째 글자가 S가 3개라는 의미입니다.

S3를 이용하면 이미지 파일을 저장할 수 있고, 저장된 이미지를 웹에서 볼 수 있습니다.

 

순서는 다음과 같습니다.

  • AWS S3에 버킷 생성
  • Cognito Identity Pool(자격 증명 풀) 생성
  • IAM에서 자격 증명 풀에 S3 권한 추가

AWS에서 S3 버킷 생성 

우선 AWS에 로그인 한 후 "S3"를 검색하여 서비스로 이동합니다.

AWS S3

 

S3를 사용하기 위해서는 아래와 같이 Bucket을 생성해 주어야 합니다.

  • 왼쪽 메뉴의 "버킷"을 클릭
  • "버킷만들기" 버튼을 클릭버킷의 이름은 S3 서비스를 통틀어 유일해야만 함.

  • 리전을 선택한 후
  • 버킷 선택을 클릭

  • 퍼블릭 액세스 차단 설정에서 "모든 퍼블릭 엑세스 차단"의 체크 박스를 해제.
    • 현재는 샘플 프로젝트이므로 보안 작업 없이 진행합니다.
    • 운영 환경에서는 권한을 반드시 설정하여 사용하시기 바랍니다.

  • "모든 퍼블릭 액세스 차단을 비활성화하면..." 아래의 체크박스를 체크.

  • "버킷 만들기" 클릭

  • 버킷 생성이 완료되면 생성된 버킷명을 클릭하고, "권한" 탭을 클릭

 

  • CORS 항목에서 "편집"을 클릭한 후 다음 내용을 입력합니다.
[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "GET",
            "PUT",
            "POST",
            "HEAD"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": [
            "x-amz-server-side-encryption",
            "x-amz-request-id",
            "x-amz-id-2"
        ],
        "MaxAgeSeconds": 3000
    }
]

Cognito Identity Pool(자격 증명 풀) 생성

  • aws에서 cognito 서비스를 검색하여 이동합니다.

  • "자격 증명 풀 관리"를 클릭.

  • "새 자격 증명 풀 만들기"를 클릭.

  • 자격 증명 풀 이름 입력(예: SliderS3Access)
  • "인증되지 않은 자격 증명에 대한 액세스 활성화" 클릭

  • "풀 생성" 버튼 클릭

  • "허용" 버튼 클릭

  • 샘플 코드의 플랫폼에서 "JavaScript"를 선택하면 아래와 같은 코드가 나타납니다.

  • 이 코드를 소스에 추가하면 업로드가 가능해집니다.

IAM에서 권한 추가

  • AWS의 IAM 서비스로 이동합니다.

  • "역할" 메뉴에서 "slider"로 검색.
  • 생성된 Role을 클릭.(예 : Cognito_SliderS3AccessAuth_Role)

  • "권한 추가" 클릭 후 "정책 연결"을 선택.

  • 기타 권한 정책에서 "s3"로 검색 후 "AmazonS3FullAccess"를 선택.
  • "정책 연결" 버튼을 클릭.

이렇게 하면 S3에 업로드 할 수 있는 권한 생성 및 설정이 완료되었습니다.

반응형
반응형

이번 시간은 front에서 jwt 토큰을 추가하여 사용자 조회를 하고, 권한 관리를 하는 기능을 구현 해 봅니다.

백엔드(slider-api)

토큰 Validation

권한 관리를 위해 JWT Token validation을 추가 합니다.

이것은 사용자가 header에 token을 보냈을때 사용자 정보를 조회하는 기능입니다.

/src/auth/security/passport.jwt.strategy.ts

async validate(payload: Payload, done: VerifiedCallback): Promise<any> {
    const user = await this.authService.tokenValidateUser(payload);
    if(!user) {
        return done(new UnauthorizedException({message: 'user does not exist'}), false);
    }
    return done(null, user);
}

authService에 tokenValidateUser를 추가합니다.

/src/auth/auth.service.ts

async tokenValidateUser(payload: Payload): Promise<User| undefined> {
    const userFind = await this.userService.findByFields({
        where: { id: payload.id }
    });
    this.flatAuthorities(userFind);
    return userFind;
}

private flatAuthorities(user: any): User {
    if (user && user.authorities) {
        const authorities: string[] = [];
        user.authorities.forEach(authority => authorities.push(authority.authorityName));
        user.authorities = authorities;
        for(const auth of authorities){
            if(auth === RoleType.ADMIN){
                user.isAdmin = true;
	        }
        }
    }
    return user;
}

로그인한 사용자만 접근할 수 있도록 Auth Guard를 만듭니다.

/src/auth/security/auth.guard.ts

import { ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
import { AuthGuard as NestAuthGuard } from "@nestjs/passport";
import { Observable } from "rxjs";

@Injectable()
export class AuthGuard extends NestAuthGuard('jwt'){
    canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
        return super.canActivate(context)
    }

    handleRequest(err, user, info) {
        if (err || !user) {
            throw err || new UnauthorizedException();
        }
        return user;
    }
}

권한별로 접근할 수 있도록 Role Guard를 만듭니다.

여기서는 일반 사용자와 관리자 권한을 체크하려고 합니다.

/src/auth/security/role.guard.ts

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { User } from '../../domain/user.entity';


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

    canActivate(context: ExecutionContext): boolean {
        const roles = this.reflector.get<string[]>('roles', context.getHandler());

        if (!roles) {
            return true;
        }

        const request = context.switchToHttp().getRequest();
        const user = request.user as User;
        console.log(`auth include : ${user.authorities.some(role => roles.includes(role))}`)

        return user && user.authorities && user.authorities.some(role => roles.includes(role));
    }
}

AuthController에서 사용자 정보를 조회하는 API를 추가합니다.

이 api는 "/auth/user"를 통해 호출할 수 있으며,

@UseGuards(AuthGuard)를 사용하였으므로 로그인한 사용자만 사용이 가능합니다.

/src/auth/auth.controller.ts

// 사용자 정보 조회
@UseGuards(AuthGuard)
@Get('/user')
getUser(@Request() req): any {
    console.log(req);
    return req.user;
}

 

프론트엔드(slider-front)

사용자 정보 조회

이제 front에서 로그인 후 사용자 정보를 조회하는 로직을 추가합니다.

 

우선 store에 user와 isAdmin을 추가합니다.

/store/index.js

export const state = () => ({
    accessToken: '',
    user: '',
});

export const mutations = {
    accessToken (state, data) {
        state.accessToken = data;
    },
    user (state, data) {
        state.user = data;
    },
}

로그인 후 api를 호출해 사용자 정보를 조회하고 store에 저장합니다.

/pages/kakao-callback.vue

const response = await this.$axios.$post(
    '/auth/login', body, {}
)
if(response){
  const accessToken = response.accessToken;
  console.log(accessToken);
  // 토큰을 스토어에 저장하기
  this.$store.commit('accessToken', accessToken);
  const options = {
    headers: {
      Authorization: `Bearer ${accessToken}`
    },
  };
  //토큰을 이용하여 사용자 정보 조회하기
  const user = await this.$axios.get("/auth/user", options);
  const loginUser = user.data;
  console.log(loginUser);
  this.$store.commit('user', loginUser);
}
return this.$router.push('/')

Navigation에서 관리자일 경우만 관리자 링크를 보여주도록 추가합니다.

/layouts/default.vue

<template>
  <div>
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand href="#">Slider</b-navbar-brand>

      <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>

      <b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav class="ml-auto" v-if="!accessToken">
          <b-nav-item href="/login" right>로그인</b-nav-item>
        </b-navbar-nav>
        <b-navbar-nav class="ml-auto" v-if="accessToken && accessToken!==''">
          <b-nav-item @click="admin" right v-if="user.isAdmin">관리자</b-nav-item>
          <b-nav-item @click="logout" right>로그아웃</b-nav-item>
        </b-navbar-nav>
      </b-collapse>
    </b-navbar>
    <nuxt/>
  </div>
</template>
<script>
export default {
  name: 'Layout',
  data() {
    return {
      auth: false
    }
  },
  mounted() {
    this.$nuxt.$on('auth', auth=>{
      this.auth = auth
      console.log(auth)
    })
  },
  methods: {
    async logout() {
      await this.$store.commit('accessToken', '');
      await this.$router.push('/');
    },
    async admin() {
      alert('관리자 메뉴!!!')
    }
  },
  computed: {
    accessToken() {
      return this.$store.state.accessToken
    },
    user() {
      return this.$store.state.user;
    },
  }
}
</script>

 

테스트

로그인 테스트를 하면 다음과 같은 결과를 확인할 수 있습니다.

 

MySQL의 user_authority 테이블에 관리자 권한을 추가합니다.

 

다시 로그인 테스트를 하면 관리자 메뉴가 추가된걸 보실 수 있습니다.

 

* 참고로 백엔드에서 admin 권한 체크를 하실때는 controller에 다음과 같이 데코레이터를 추가하시면 됩니다.

@UseGuards(AuthGuard,RolesGuard)
@Roles(RoleType.ADMIN)

 

반응형
반응형

아래는 이 글의 동영상 강좌입니다.

https://youtu.be/QO1NunXmDuU

 

Jwt Token을 이용하여 frontend에서 로그인을 하는 방법을 알아보겠습니다.

 

Store 기능 사용하기

로그인 정보 저장을 위해 store를 사용하도록 합니다.

store 폴더 아래에 index.js 를 추가하고 다음 코드를 입력합니다.

/store/index.js

export const state = () => ({
    accessToken: {}
});

export const mutations = {
    accessToken (state, data) {
        state.accessToken = data;
    }
}

 

Navbar 만들기

Navbar를 만드는 다양한 방법이 있습니다. 이번엔 bootstrap vue를 이용하도록 합니다.

Nuxt 프로젝트에 bootstrap vue를 사용하는 방법은 다음 사이트를 참조하세요. https://bootstrap-vue.org/docs 

아래 명령으로 slider-front 프로젝트에 설치를 합니다.

yarn add bootstrap-vue

nuxt-config.js에 다음 설정을 추가합니다.

modules = {
  'bootstrap-vue/nuxt'
}

프로젝트에 layouts 폴더를 만들고 default.vue를 만듭니다.

bootstrap vue 사이트의 navbar 샘플을 복사하여 default.vue의 template에 붙여놓고 아래와 같이 변경합니다.

<template>
  <div>
  <b-navbar toggleable="lg" type="dark" variant="info">
    <b-navbar-brand href="#">Slider</b-navbar-brand>

    <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>

    <b-collapse id="nav-collapse" is-nav>
      <b-navbar-nav class="ml-auto" v-if="!accessToken">
        <b-nav-item href="/login" right>로그인</b-nav-item>
      </b-navbar-nav>
      <b-navbar-nav class="ml-auto" v-if="accessToken && accessToken!==''">
        <b-nav-item @click="logout" right>로그아웃</b-nav-item>
      </b-navbar-nav>
    </b-collapse>
  </b-navbar>
  <nuxt/>
  </div>
</template>
<script>
export default {
  name: 'Layout',
  methods: {
    async logout() {
      await this.$store.commit('accessToken', '');
      await this.$router.push('/');
    }
  },
  computed: {
    accessToken() {
      return this.$store.state.accessToken
    }
  }
}
</script>

 

Login

기존 index.vue를 login.vue로 변경합니다.

index.vue를 만들고 다음과 같이 입력합니다.

index 페이지에서 로그인이 잘 되었는지를 확인하기 위해 accessToken을 보여주는 예제입니다.

/pages/index.vue

<template>
  <div>
    <div>Welcome!</div>
    <div v-if="accessToken">{{accessToken}}</div>
  </div>
</template>
<script>
export default {
  name: 'IndexPage',
  computed: {
    accessToken() {
      return this.$store.state.accessToken
    }
  }
}
</script>

로그인 API 호출 로직 수정

pages/kakao-callback.vue를 다음과 같이 수정합니다.

<template>
  <div>로그인중....</div>
</template>

<script>
  export default {
    async mounted() {
      try{
        if(!this.$route.query.code){
          return this.$router.push('/')
        }
        console.log(`code : ${this.$route.query.code}`)
        const body = {
          code: this.$route.query.code,
          domain: window.location.origin
        }
        const response = await this.$axios.$post(
            '/auth/login', body, {}
        )
        if(response){
          console.log(response.accessToken);
          this.$store.commit('accessToken', response.accessToken);
        }
        return this.$router.push('/')
      }catch(error){
        console.log(error)
      }
    }
  }
</script>

프로젝트 실행화면

프로젝트를 실행하면 다음과 같은 화면을 보실 수 있습니다.

 

첫화면

 

로그인 화면

 

로그인 성공 후 화면

 

반응형
반응형

이번 시간은 JWT 인증 기능을 추가해 보겠습니다.

 

다음은 이 강의의 동영상입니다.

https://youtu.be/SClJMs2BlWk

 

 

JWT에 관한 자세한 사항은 제가 이전에 작성한 글을 참조해 주세요.

https://codegear.tistory.com/72

 

[Nodejs 프레임워크]NestJS - 12. JWT 토큰 생성

다음은 이글의 동영상 강의 입니다. https://youtu.be/H4VS-Osylvo 토큰을 이용한 API 호출 방법 REST API 호출시 토큰을 이용하면 사용자 인증과 권한을 체크할 수 있습니다. 다음 그림은 토큰의 생성과 호

codegear.tistory.com

 

JWT 관련 패키지 추가

jwt 인증을 위해서는 다음과 같은 패키지가 필요합니다.

- @nestjs/jwt

- @nestjs/passport

- passport

- passport-jwt

아래와 같이 패키지를 추가합니다.

yarn add @nestjs/jwt @nestjs/passport passport passport-jwt

 

Payload 인터페이스 생성

jwt인증시엔 payload interface가 필요합니다.

아래와 같이 같이 Payload interface를 만듭니다.(참조 : https://jwt.io/)

/src/auth/security/payload.interface.ts

export interface Payload {
    id: number;
    name: string;
    authorities?: any[];
}

JwtStrategy 를 아래와 같이 작성합니다.

/src/auth/security/passport.jwt.stategy.ts

import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import { AuthService } from "../auth.service";

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

 

로그인 및 회원 가입

카카오 로그인 후에 회원 로그인 및 회원 가입을 처리합니다.

- 회원 데이터가 없을 경우는 회원 가입을 처리합니다.

- 회원 데이터가 있을 경우는 로그인 처리를 합니다.

 

우선 auth.controller에서 카카오 로그인을 한 후 다음 코드를 추가합니다.

/src/auth/auth.controller.ts

			console.log(`kakaoUserInfo : ${JSON.stringify(kakao)}`);
            if (!kakao.id) {
                throw new BadRequestException('카카오 로그인 실패!');
            }

            // 로그인 처리 - 회원 가입이 안되어 있을 경우 가입 처리
            const jwt =  await this.authService.login(kakao);
            console.log(`jwt.accessToken : ${jwt.accessToken}`);
            res.send({
                accessToken: jwt.accessToken,
                message: 'success'
            });

authService에서 고객을 조회하고, 로그인을 처리합니다.

/src/auth/auth.service.ts

async login(kakao): Promise<{accessToken: string} | undefined> {
	let userFind: User = await this.userService.findByFields({
            where: { kakaoId: kakao.id }
        });
        if(!userFind) {
            isFirstLogin = true;
            // 회원 가입
            const user = new User();
            user.kakaoId = kakao.id;
            user.email = kakao.kakao_account.email;
            user.name = kakao.kakao_account.name;
            
            userFind = await this.registerUser(user);
        }

        const payload: Payload = {
            id: userFind.id,
            name: userFind.name,
            authorities: userFind.authorities
        };
        return {
            accessToken: this.jwtService.sign(payload)
        };
}

userService에  findByFields, registerUser를 추가합니다.

/src/auth/user.service.ts

   constructor(
        @InjectRepository(User)
        private userRepository: Repository<User>,
        @InjectRepository(UserAuthority)
        private userAuthorityRepository: Repository<UserAuthority>,
    ){}
    
    async findByFields(options: FindOneOptions<User>): Promise<User | undefined> {
        return await this.userRepository.findOne(options);
    }
    
    async registerUser(newUser: User): Promise<User> {
        let userFind: User = await this.findByFields({
            where: { kakaoId: newUser.kakaoId }
        });
        if(userFind) {
            throw new HttpException('Username aleady used!', HttpStatus.BAD_REQUEST);
        }
        const registeredUser = await this.userService.save(newUser);
        if(registeredUser){
            await this.saveAuthority(registeredUser.id);
        }else {
            throw new HttpException('Username register error!', HttpStatus.INTERNAL_SERVER_ERROR);
        }

        return registeredUser;
    }
    
    async save(user: User): Promise<User | undefined> {
        return await this.userRepository.save(user);
    }
    
    async saveAuthority(userId: number): Promise<UserAuthority | undefined> {
        let userAuth = new UserAuthority();
        userAuth.userId = userId;
        userAuth.authorityName = RoleType.USER;
        return await this.userAuthorityRepository.save(userAuth);
    }

권한 Type을 선언한 RoleType을 만들어줍니다.

/src/auth/role-type.ts

export enum RoleType {
    USER = 'ROLE_USER',
    ADMIN = 'ROLE_ADMIN',
}

 

auth.module에 jwtModule을 추가하고 import 합니다.

/src/auth/auth.module.ts

import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './security/passport.jwt.strategy';
...
@Module({
  imports: [
    TypeOrmModule.forFeature([User]),
    JwtModule.register({
      secret: process.env.JWT_SECRET,
      signOptions: {expiresIn: '300s'},
    }),
    PassportModule
  ],
  exports: [TypeOrmModule, UserService],
  controllers: [AuthController],
  providers: [AuthService, UserService, JwtStrategy]
})
export class AuthModule {}

 

프론트에서 로그인을 하면 브라우저의 콘솔에서 아래와 같이 jwt token을 확인할 수 있습니다.

반응형

+ Recent posts