diff --git a/serverless.yml b/serverless.yml index c753cc7..468ba45 100644 --- a/serverless.yml +++ b/serverless.yml @@ -44,7 +44,7 @@ functions: handler: src/handler.getAlgorithmCountAtAll events: - http: - path: algoirthm/count + path: algorithm/count method: get cors: true @@ -52,7 +52,7 @@ functions: handler: src/handler.getAlgorithmRules events: - http: - path: algoirthm/rule + path: algorithm/rule method: get cors: true @@ -60,7 +60,7 @@ functions: handler: src/handler.getAlgorithmRulesForWeb events: - http: - path: algoirthm/rule/web + path: algorithm/rule/web method: get cors: true @@ -68,7 +68,7 @@ functions: handler: src/handler.getAlgorithmList events: - http: - path: algoirthm/list + path: algorithm/list method: get cors: true @@ -76,7 +76,7 @@ functions: handler: src/handler.getAlgorithmListAtPages events: - http: - path: algoirthm/page + path: algorithm/page method: get cors: true @@ -84,7 +84,7 @@ functions: handler: src/handler.deleteAlgorithm events: - http: - path: algoirthm/{id} + path: algorithm/{id} method: delete cors: true @@ -92,7 +92,7 @@ functions: handler: src/handler.modifyAlogirithemContent events: - http: - path: algoirthm/{id} + path: algorithm/{id} method: patch cors: true @@ -100,7 +100,7 @@ functions: handler: src/handler.setAlgorithmStatus events: - http: - path: algoirthm/{id}/status + path: algorithm/{id}/status method: patch cors: true diff --git a/src/DTO/algorithm.dto.ts b/src/DTO/algorithm.dto.ts index 8ec26c3..5e1f039 100644 --- a/src/DTO/algorithm.dto.ts +++ b/src/DTO/algorithm.dto.ts @@ -1,13 +1,24 @@ -export interface BaseAlgorithmDTO { - title: string; - content: string; - tag: string; -} - -export type AlgorithmStatusType = - | "PENDING" - | "ACCEPTED" - | "REJECTED" - | "REPORTED"; - -export interface ModifyAlgorithmDTO extends BaseAlgorithmDTO {} +export interface BaseAlgorithmDTO { + title: string; + content: string; + tag: string; +} + +export interface GeneratedAlgorithmDTO extends BaseAlgorithmDTO { + number: number; +} +export type AlgorithmStatusType = + | "PENDING" + | "ACCEPTED" + | "REJECTED" + | "REPORTED"; + +export interface JoinAlgorithmDTO { + count: number; + cursor?: number; + page?: number; + status: AlgorithmStatusType; + isAdmin: boolean; +} + +export interface ModifyAlgorithmDTO extends BaseAlgorithmDTO {} diff --git a/src/DTO/discord.dto.ts b/src/DTO/discord.dto.ts new file mode 100644 index 0000000..252a0a7 --- /dev/null +++ b/src/DTO/discord.dto.ts @@ -0,0 +1,201 @@ +import { BaseAlgorithmDTO } from "./algorithm.dto"; + +export interface DiscordObject { + id: string; +} +export interface DiscordWebhook extends DiscordObject { + type: number; + name: string; + avatar: string; + application_id: string; + + guild_id?: string | null; + channel_id: string | null; +} + +export interface DiscordIncomingWebhook extends DiscordWebhook { + token?: string; + user?: DiscordUser; +} +export interface DiscordChannelFollowerWebhook extends DiscordWebhook { + user?: DiscordUser; + source_guild?: DiscordGuild; + source_channel?: DiscordChannel; + url?: string; +} +export interface DiscordApplicationWebhook extends DiscordWebhook {} + +interface DiscordGuild extends DiscordObject {} // TODO +interface DiscordChannel extends DiscordObject {} // TODO + +export interface DiscordUser extends DiscordObject { + username: string; + discriminator: string; + avatar?: string; + bot?: boolean; + system?: boolean; + mfa_enabled?: boolean; + banner?: string; + accent_color?: number; + locale?: string; + verified?: boolean; + email?: string; + flags?: number; + premium_type?: number; + public_flags?: number; +} + +export interface SendDiscordWebhookMessage { + content?: string; // content, file, embeds 셋 중 하나는 필수 + username?: string; + avatar_url?: string; + tts?: boolean; + file?: File; // content, file, embeds 셋 중 하나는 필수 + embeds?: DiscordEmbed[]; // content, file, embeds 셋 중 하나는 필수 + payload_json?: string; // multipart/form-data only + allowed_mentions?: DiscordAllowedMentions; + components?: DiscordComponent[]; +} + +export interface DiscordEmbed { + title?: string; + type: DiscordEmbedType; // webhook 사용시 항상 rich + description?: string; + url?: string; + timestamp?: string; // ISO8601 timestamp + color?: number; + footer?: DiscordEmbedFooter; + image?: DiscordEmbedImage; + thumbnail?: DiscordEmbedThumbnail; + video?: DiscordEmbedVideo; + provider?: DiscordEmbedProvider; + author?: DiscordEmbedAuthor; + fields?: DiscordEmbedFields[]; +} +interface DiscordEmbedLinkedComponent { + url?: string; +} +interface DiscordEmbedLinkedMediaContent extends DiscordEmbedLinkedComponent { + proxy_url?: string; + height?: number; + width?: number; +} +export interface DiscordEmbedFooter { + text: string; + icon_url?: string; + proxy_icon_url?: string; +} +export interface DiscordEmbedImage extends DiscordEmbedLinkedMediaContent {} +export interface DiscordEmbedThumbnail extends DiscordEmbedLinkedMediaContent {} +export interface DiscordEmbedVideo extends DiscordEmbedLinkedMediaContent {} +export interface DiscordEmbedProvider extends DiscordEmbedLinkedComponent { + name?: string; +} +export interface DiscordEmbedAuthor extends DiscordEmbedLinkedComponent { + name?: string; + icon_url?: string; + proxy_icon_url?: string; +} +export interface DiscordEmbedFields { + name: string; + value: string; + inline?: boolean; +} + +export const DiscordEmbedType = { + rich: "rich", + image: "image", + video: "video", + gifv: "gifv", + article: "article", + link: "link", +} as const; +export type DiscordEmbedType = + typeof DiscordEmbedType[keyof typeof DiscordEmbedType]; + +export interface DiscordAllowedMentions { + parse?: AllowedMentionTypes[]; + roles?: string[]; + users?: string[]; + replied_user?: boolean; +} + +export type AllowedMentionTypes = "roles" | "users" | "everyone"; + +export type DiscordButtonsComponentType = { + type: DiscordEmbedComponentType; + custom_id?: string; + disabled?: boolean; + style?: number; + label?: string; + emoji?: DiscordEmoji; + url?: string; +}; +export type DiscordSelectMenusComponentType = { + type: DiscordEmbedComponentType; + custom_id?: string; + disabled?: boolean; + + options: SelectOptions[]; + placeholder?: string; + min_values?: number; + max_values?: number; +}; +export type DiscordActionRowsComponentType = { + type: DiscordEmbedComponentType; + components?: DiscordComponent[]; +}; +export type DiscordComponent = + | DiscordButtonsComponentType + | DiscordSelectMenusComponentType + | DiscordActionRowsComponentType; + +export const DiscordEmbedComponentType = { + Action_Row: 1, + Button: 2, + Select_Menu: 3, +} as const; +export type DiscordEmbedComponentType = + typeof DiscordEmbedComponentType[keyof typeof DiscordEmbedComponentType]; + +export interface SelectOptions { + label: string; + value: string; + description?: string; + emoji?: DiscordEmoji; + default?: boolean; +} +export interface DiscordEmoji { + id: string | null; + name: string | null; + roles?: DiscordRoles[]; + user?: DiscordUser; + require_colons?: boolean; + managed?: boolean; + animatd?: boolean; + available?: boolean; +} + +export interface DiscordRoles { + id: string; + name: string; + color: number; + hoist: boolean; + position: number; + permissions: string; + managed: boolean; + mentionalbe: boolean; + tags?: DiscordRoleTags; +} +export interface DiscordRoleTags { + bot_id?: string; + integration_id?: string; + premium_subscriber?: null; +} + +export interface GenerateMessage { + form: BaseAlgorithmDTO; + coment: string; + color: number; + description: string; +} diff --git a/src/middleware/database.ts b/src/middleware/database.ts index 22cf123..6e9e7ca 100644 --- a/src/middleware/database.ts +++ b/src/middleware/database.ts @@ -11,7 +11,9 @@ export class DBMiddleware { // run function const result = await originMethod.apply(this, args); - await connection.close(); + if (result) { + await connection.close(); + } return result; }; diff --git a/src/repository/algorithm.ts b/src/repository/algorithm.ts index 302b5da..bd1efe1 100644 --- a/src/repository/algorithm.ts +++ b/src/repository/algorithm.ts @@ -1,10 +1,44 @@ -import { EntityRepository, Repository } from "typeorm"; -import { ModifyAlgorithmDTO } from "../DTO/algorithm.dto"; +import { EntityRepository, Repository, SelectQueryBuilder } from "typeorm"; +import { JoinAlgorithmDTO, ModifyAlgorithmDTO } from "../DTO/algorithm.dto"; import { Algorithm } from "../entity"; @EntityRepository(Algorithm) export class AlgorithmRepository extends Repository { + getListByCursor({ count, cursor, status, isAdmin }: JoinAlgorithmDTO) { + function addOptions(algorithmList: SelectQueryBuilder) { + return algorithmList.take(count).orderBy("postNumber", "DESC").getMany(); + } + + const base = this.createQueryBuilder("algorithm").where( + "algorithm.algorithmStatus = :status", + { + status: isAdmin ? status : "PENDING", + } + ); + + return addOptions( + !!cursor + ? base.andWhere("algorithm.postNumber <= :cursor", { cursor }) + : base + ); + } + + getListByPage({ count, page, status, isAdmin }: JoinAlgorithmDTO) { + function addOptions(algorithmList: SelectQueryBuilder) { + return algorithmList.take(count).orderBy("postNumber", "DESC").getMany(); + } + + const base = this.createQueryBuilder("algorithm").where( + "algorithm.algorithmStatus = :status", + { + status: isAdmin ? status : "PENDING", + } + ); + + return addOptions(!!page ? base.skip((page - 1) * count) : base); + } + async getAlgorithmCountAtAll() { return this.createQueryBuilder("algorithm") .select("algorithm.algorithmStatus AS status") diff --git a/src/router/algorithm/algorithm.service.ts b/src/router/algorithm/algorithm.service.ts index 4dfa3d4..cadfaab 100644 --- a/src/router/algorithm/algorithm.service.ts +++ b/src/router/algorithm/algorithm.service.ts @@ -1,13 +1,18 @@ import { APIGatewayEvent } from "aws-lambda"; -import { getCustomRepository, getRepository } from "typeorm"; +import { JwtPayload } from "jsonwebtoken"; +import { getConnection, getCustomRepository, getRepository } from "typeorm"; +import { AlgorithmStatusType, BaseAlgorithmDTO, ModifyAlgorithmDTO } from "../../DTO/algorithm.dto"; import { bold13, bold15, ruleForWeb, rules } from "../../config"; -import { BaseAlgorithmDTO, ModifyAlgorithmDTO } from "../../DTO/algorithm.dto"; -import { Algorithm } from "../../entity"; +import { Algorithm, User } from "../../entity"; + import { AlgorithmRepository } from "../../repository/algorithm"; +import { UserRepository } from "../../repository/user"; + import { getLastPostNumber } from "../../util/algorithm"; import { createErrorRes, createRes } from "../../util/http"; import { isNumeric } from "../../util/number"; +import { verifyToken } from "../../util/token"; export const AlgorithmService: { [k: string]: Function } = { writeAlgorithm: async ({ title, content, tag }: BaseAlgorithmDTO) => { @@ -28,12 +33,68 @@ export const AlgorithmService: { [k: string]: Function } = { return createErrorRes({ status: 500, errorCode: "JL004" }); } }, + + getAlgorithmList: async (event: APIGatewayEvent) => { + const { count, cursor, status } = event.queryStringParameters; + if (!isNumeric(count)) { + return createErrorRes({ errorCode: "JL007" }); + } + + const userTokens = verifyToken( + event.headers.Authorization ?? event.headers.authorization + ) as JwtPayload; + + let isAdmin = false; + if (userTokens !== null) { + const userRepo = getCustomRepository(UserRepository); + isAdmin = await userRepo.getIsAdminByEmail(userTokens.email); + } + + const algorithmRepo = getCustomRepository(AlgorithmRepository); + const algorithmList = await algorithmRepo.getListByCursor({ + count: Number(count) === 0 ? null : Number(count), + cursor: parseInt(cursor), + status: status as AlgorithmStatusType, + isAdmin, + }); + + return createRes({ body: algorithmList }); + }, + getAlgorithmListAtPage: async (event: APIGatewayEvent) => { + const { count, page, status } = event.queryStringParameters; + + if (!isNumeric(count) || !isNumeric(page)) { + return createErrorRes({ errorCode: "JL007" }); + } + + const userTokens = verifyToken( + event.headers.Authorization ?? event.headers.authorization + ) as JwtPayload; + + let isAdmin = false; + if (userTokens !== null) { + const userRepo = getCustomRepository(UserRepository); + isAdmin = await userRepo.getIsAdminByEmail(userTokens.email); + } + const algorithmRepo = getCustomRepository(AlgorithmRepository); + + const algorithmList = await algorithmRepo.getListByPage({ + count: Number(count) === 0 ? null : Number(count), + page: parseInt(page), + status: status as AlgorithmStatusType, + isAdmin, + }); + + return createRes({ body: algorithmList }); + }, + getAlgorithmCountAtAll: async () => { const result = await getCustomRepository( AlgorithmRepository ).getAlgorithmCountAtAll(); return createRes({ body: result }); }, + getAlgorithmRules: () => { return createRes({ body: { @@ -43,6 +104,7 @@ export const AlgorithmService: { [k: string]: Function } = { }, }); }, + getAlgorithmRulesForWeb: () => { return createRes({ body: { @@ -50,6 +112,7 @@ export const AlgorithmService: { [k: string]: Function } = { }, }); }, + modifyAlgorithmContent: async (event: APIGatewayEvent) => { const { id } = event.pathParameters; @@ -63,6 +126,7 @@ export const AlgorithmService: { [k: string]: Function } = { body: await algorithmRepo.modifyAlgorithm(Number(id), data), }); }, + deleteAlgorithm: async (event: APIGatewayEvent) => { const { id } = event.pathParameters; diff --git a/src/router/algorithm/index.ts b/src/router/algorithm/index.ts index 90c5ab2..95d61b6 100644 --- a/src/router/algorithm/index.ts +++ b/src/router/algorithm/index.ts @@ -4,6 +4,22 @@ import { DBMiddleware } from "../../middleware/database"; import { AlgorithmService } from "./algorithm.service"; export class AlgorithmRouter { + static async getAlgorithmCountAtAll() {} + static async getAlgorithmRules() {} + static async getAlgorithmRulesForWeb() {} + + @AuthMiddleware.onlyOrigin + @DBMiddleware.connectTypeOrm + static async getAlgorithmList(event: APIGatewayEvent, _: any) { + return AlgorithmService.getAlgorithmList(event); + } + + @AuthMiddleware.onlyOrigin + @DBMiddleware.connectTypeOrm + static async getAlgorithmListAtPages(event: APIGatewayEvent, _: any) { + return AlgorithmService.getAlgorithmListAtPage(event); + } + @AuthMiddleware.onlyOrigin @DBMiddleware.connectTypeOrm static async getAlgorithmCountAtAll(_: APIGatewayEvent, __: any) { @@ -18,9 +34,6 @@ export class AlgorithmRouter { return AlgorithmService.getAlgorithmRulesForWeb(); } - static async getAlgorithmList() {} - static async getAlgorithmListAtPages() {} - @AuthMiddleware.onlyOrigin @DBMiddleware.connectTypeOrm @AuthMiddleware.authUserByVerifyQuestionOrToken diff --git a/src/util/algorithm.ts b/src/util/algorithm.ts index cce4a2c..d0bfce0 100644 --- a/src/util/algorithm.ts +++ b/src/util/algorithm.ts @@ -1,5 +1,5 @@ import { getRepository } from "typeorm"; -import { AlgorithmStatusType } from "../DTO/algorithm.dto"; +import { AlgorithmStatusType, JoinAlgorithmDTO } from "../DTO/algorithm.dto"; import { Algorithm } from "../entity"; export const getLastPostNumber: Function = async ( diff --git a/src/util/discord.ts b/src/util/discord.ts new file mode 100644 index 0000000..0b5d169 --- /dev/null +++ b/src/util/discord.ts @@ -0,0 +1,123 @@ +import got from "got"; + +import { BaseAlgorithmDTO, GeneratedAlgorithmDTO } from "../DTO/algorithm.dto"; +import { + DiscordEmbed, + DiscordEmbedFooter, + DiscordEmbedType, + GenerateMessage, + SendDiscordWebhookMessage, +} from "../DTO/discord.dto"; + +const generateWebhookMessage: Function = ({ + form, + coment, + description, + color, +}: GenerateMessage): SendDiscordWebhookMessage => { + const footerData: DiscordEmbedFooter = { + text: form.tag, + icon_url: + "https://cdn.discordapp.com/avatars/826647881800351765/0493a57e7c5a21dd4e434a153d44938e.webp?size=128", + }; + return { + embeds: [ + { + type: DiscordEmbedType.rich, + title: coment, + description: description, + fields: [ + { + name: form.title, + value: form.content, + inline: false, + }, + ], + footer: footerData, + color: color, + }, + ], + }; +}; + +export const sendNewAlgorithemMessage: Function = async ( + data: BaseAlgorithmDTO +): Promise => { + const message: SendDiscordWebhookMessage = generateWebhookMessage({ + form: data, + coment: "알고리즘 갱신!", + description: "새로운 알고리즘이 기다리고있습니다!", + color: 1752220, + }); + await sendMessage(process.env.DISCORD_MANAGEMENT_WEBHOOK, message); +}; + +export const sendSetRejectedMessage: Function = async ( + data: BaseAlgorithmDTO, + reason: string +): Promise => { + const changeReason = reason ? `\n**거절 사유** : ${reason}` : ""; + const message: SendDiscordWebhookMessage = generateWebhookMessage({ + form: { title: data.title, content: " ", tag: data.tag }, + coment: "거절된 알고리즘", + description: `해당 알고리즘이 거절되었습니다.${changeReason}`, + color: 16711680, + }); + await sendMessage(process.env.DISCORD_RECJECTED_WEBHOOK, message); +}; + +export const sendReportMessage: Function = async ( + data: BaseAlgorithmDTO, + reason: string +): Promise => { + const changeReason = reason ? `\n**신고 사유** : ${reason}` : ""; + const message: SendDiscordWebhookMessage = generateWebhookMessage({ + form: data, + coment: "알고리즘 신고", + description: `해당 알고리즘이 신고되었습니다.${changeReason}`, + color: 16711680, + }); + await sendMessage(process.env.DISCORD_REPORT_WEBHOOK, message); +}; + +export const sendACCEPTEDAlgorithemMessage: Function = async ( + data: BaseAlgorithmDTO +): Promise => { + const message: SendDiscordWebhookMessage = generateWebhookMessage({ + form: data, + coment: "알고리즘 갱신!", + description: "새로운 알고리즘이 기다리고있습니다!", + color: 1752220, + }); + await sendMessage(process.env.DISCORD_ACCEPTED_WEBHOOK, message); +}; + +export const algorithemDeleteEvenetMessage: Function = async ( + post: GeneratedAlgorithmDTO, + reason: string +): Promise => { + const deletedReason: string = reason ? `\n**삭제 사유** : ${reason}` : ""; + const message: SendDiscordWebhookMessage = generateWebhookMessage({ + form: { + title: post.title, + content: post.content, + tag: post.tag, + }, + coment: "알고리즘이 삭제됨", + description: `${post.number}번째 알고리즘이 삭제되었습니다.${deletedReason}`, + color: 16711680, + }); + await sendMessage(process.env.DISCORD_ABOUT_DELETE_WEBHOOK, message); +}; + +const sendMessage: Function = async ( + url: string, + data: DiscordEmbed +): Promise => { + const _res = await got.post(url, { + json: data, + headers: { + "Content-Type": "application/json", + }, + }); +}; diff --git a/src/util/http.ts b/src/util/http.ts index 5d25d20..f54cc05 100644 --- a/src/util/http.ts +++ b/src/util/http.ts @@ -1,69 +1,62 @@ -import { CreateResInput, ReturnResHTTPData } from "../DTO/http.dto"; - -export const ALLOWED_ORIGINS: string[] = [ - "http://localhost:3000", - "https://localhost:3000", - "http://localhost", - "https://localhost", - "https://joog-lim.info", - "https://www.joog-lim.info", - "https://jooglim.netlify.app", -]; - -export const ERROR_CODE_LIST: { [key in ErrorCodeType]: string } = { - JL001: "인가되지않은 Origin입니다.", - JL002: "어드민이 아닙니다.", - JL003: "인자값이 부족합니다.", - JL004: "예상치 못한 에러입니다. 개발자에게 문의해주세요.", - JL005: "Token 값을 찾을수 없습니다.", - JL006: "Token 인증이 실패하였습니다.", - JL007: "잘못된 요청입니다.", -} as const; - -export type ErrorCodeType = - | "JL001" - | "JL002" - | "JL003" - | "JL004" - | "JL005" - | "JL006" - | "JL007"; - -export const createRes = ({ - statusCode, - headers, - body, -}: CreateResInput): ReturnResHTTPData => { - return { - statusCode: statusCode ?? 200, - headers: Object.assign( - {}, - { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Credentials": true, - }, - headers ?? {} - ), - body: JSON.stringify(body ?? {}), - }; -}; -export const createErrorRes = ({ - errorCode, - status, -}: { - errorCode: ErrorCodeType; - status?: number; -}): ReturnResHTTPData => { - return { - statusCode: status ?? 400, - headers: { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Credentials": true, - }, - body: JSON.stringify({ - success: false, - errorCode: errorCode, - message: ERROR_CODE_LIST[errorCode], - }), - }; -}; +import { CreateResInput, ReturnResHTTPData } from "../DTO/http.dto"; + +export const ALLOWED_ORIGINS: string[] = [ + "http://localhost:3000", + "https://localhost:3000", + "http://localhost", + "https://localhost", + "https://joog-lim.info", + "https://www.joog-lim.info", + "https://jooglim.netlify.app", +]; + +export const ERROR_CODE_LIST = { + JL001: "인가되지않은 Origin입니다.", + JL002: "어드민이 아닙니다.", + JL003: "인자값이 부족합니다.", + JL004: "예상치 못한 에러입니다. 개발자에게 문의해주세요.", + JL005: "Token 값을 찾을수 없습니다.", + JL006: "Token 인증이 실패하였습니다.", + JL007: "잘못된 요청입니다.", +} as const; + +export type ErrorCodeType = keyof typeof ERROR_CODE_LIST; + +export const createRes = ({ + statusCode, + headers, + body, +}: CreateResInput): ReturnResHTTPData => { + return { + statusCode: statusCode ?? 200, + headers: Object.assign( + {}, + { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Credentials": true, + }, + headers ?? {} + ), + body: JSON.stringify(body ?? {}), + }; +}; +export const createErrorRes = ({ + errorCode, + status, +}: { + errorCode: ErrorCodeType; + status?: number; +}): ReturnResHTTPData => { + return { + statusCode: status ?? 400, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Credentials": true, + }, + body: JSON.stringify({ + success: false, + errorCode: errorCode, + message: ERROR_CODE_LIST[errorCode], + }), + }; +}; diff --git a/src/util/number.ts b/src/util/number.ts index 2039484..a98c7df 100644 --- a/src/util/number.ts +++ b/src/util/number.ts @@ -1,2 +1,3 @@ -export const isNumeric: Function = (data: string): boolean => - !isNaN(Number(data)); +xport const isNumeric: Function = (data: string): boolean => { + return !isNaN(Number(data)); +};