From 4e979831f34099658b97c2b319c0d399d79e9bc7 Mon Sep 17 00:00:00 2001 From: wonderlandpark Date: Thu, 6 May 2021 22:25:42 +0900 Subject: [PATCH] feat: added owner transfer and edit --- pages/api/v2/bots/[id]/owners.ts | 42 ++++++++++++++++++++++++++++++++ pages/manage/[id].tsx | 39 ++++++++++++++++++++++++----- utils/Csrf.ts | 4 +-- utils/Jwt.ts | 3 ++- utils/Query.ts | 8 +++++- utils/Yup.ts | 23 ++++++++++++----- 6 files changed, 103 insertions(+), 16 deletions(-) create mode 100644 pages/api/v2/bots/[id]/owners.ts diff --git a/pages/api/v2/bots/[id]/owners.ts b/pages/api/v2/bots/[id]/owners.ts new file mode 100644 index 0000000..962f33c --- /dev/null +++ b/pages/api/v2/bots/[id]/owners.ts @@ -0,0 +1,42 @@ +import { NextApiRequest } from 'next' + +import RequestHandler from '@utils/RequestHandler' +import { CaptchaVerify, get, update } from '@utils/Query' +import ResponseWrapper from '@utils/ResponseWrapper' +import { checkToken } from '@utils/Csrf' +import { EditBotOwner, EditBotOwnerSchema } from '@utils/Yup' +import { User } from '@types' + +const BotOwners = RequestHandler() + .patch(async (req: PostApiRequest, res) => { + const user = await get.Authorization(req.cookies.token) + if (!user) return ResponseWrapper(res, { code: 401 }) + const bot = await get.bot.load(req.query.id) + if(!bot) return ResponseWrapper(res, { code: 404 }) + if((bot.owners as User[])[0].id !== user) return ResponseWrapper(res, { code: 403 }) + const validated = await EditBotOwnerSchema.validate(req.body, { abortEarly: false }) + .then(el => el) + .catch(e => { + ResponseWrapper(res, { code: 400, errors: e.errors }) + return null + }) + if(!validated) return + const csrfValidated = checkToken(req, res, validated._csrf) + if (!csrfValidated) return + const captcha = await CaptchaVerify(validated._captcha) + if(!captcha) return + const userFetched: User[] = await Promise.all(validated.owners.map((u: string) => get.user.load(u))) + if(userFetched.indexOf(null) !== -1) return ResponseWrapper(res, { code: 400, message: '올바르지 않은 유저 ID를 포함하고 있습니다.' }) + if(userFetched.length > 1 && userFetched[0].id !== (bot.owners as User[])[0].id) return ResponseWrapper(res, { code: 400, errors: ['소유자를 이전할 때는 다른 관리자를 포함할 수 없습니다.'] }) + await update.botOwners(bot.id, validated.owners) + return ResponseWrapper(res, { code: 200 }) + }) + +interface PostApiRequest extends NextApiRequest { + query: { + id: string + }, + body: EditBotOwner +} + +export default BotOwners \ No newline at end of file diff --git a/pages/manage/[id].tsx b/pages/manage/[id].tsx index 7615a73..2e070c4 100644 --- a/pages/manage/[id].tsx +++ b/pages/manage/[id].tsx @@ -177,7 +177,20 @@ const ManageBotPage:NextPage = ({ bot, user, csrfToken, theme }) setAdminModal(false)} closeIcon> - alert(JSON.stringify(v.owners.map(el => el.id)))}> + { + const res = await Fetch(`/bots/${bot.id}/owners`, { method: 'PATCH', body: JSON.stringify({ + _captcha: v._captcha, + _csrf: csrfToken, + owners: v.owners.map(el => el.id) + }) }) + if(res.code === 200) { + alert('성공적으로 수정했습니다.') + setAdminModal(false) + } else { + alert(res.message) + setAdminModal(false) + } + }}> { ({ values, setFieldValue }) =>
@@ -223,7 +236,7 @@ const ManageBotPage:NextPage = ({ bot, user, csrfToken, theme }) setFieldValue('_captcha', k)} /> - + }
@@ -237,11 +250,25 @@ const ManageBotPage:NextPage = ({ bot, user, csrfToken, theme }) setTransferModal(false)} closeIcon> - alert(JSON.stringify(v))}> + { + const res = await Fetch(`/bots/${bot.id}/owners`, { method: 'PATCH', body: JSON.stringify({ + _captcha: v._captcha, + _csrf: csrfToken, + owners: [ v.ownerID ] + }) }) + if(res.code === 200) { + alert('성공적으로 소유권을 이전했습니다.') + router.push('/') + } else { + alert(res.message) + setTransferModal(false) + } + }}> { ({ values, setFieldValue }) =>
-

봇의 소유권을 이전하게 되면 봇의 소유자 권한을 이전하게 됩니다.

+

주의해주세요!

+

봇의 소유권을 이전하게 되면 봇의 소유자 권한을 이전하게 되며, 본인을 포함한 모든 관리자가 해당 봇에 대한 권한을 잃게됩니다.

이전하실 유저 ID를 입력해주세요.

@@ -267,11 +294,11 @@ const ManageBotPage:NextPage = ({ bot, user, csrfToken, theme }) setDeleteModal(false)} closeIcon> { const res = await Fetch(`/bots/${bot.id}`, { method: 'DELETE', body: JSON.stringify(v) }) - if(res.code !== 200) alert(res.message) - else { + if(res.code === 200) { alert('성공적으로 삭제하였습니다.') redirectTo(router, '/') } + else alert(res.message) }}> { ({ values, setFieldValue }) => diff --git a/utils/Csrf.ts b/utils/Csrf.ts index fa66bcd..df694ee 100644 --- a/utils/Csrf.ts +++ b/utils/Csrf.ts @@ -21,9 +21,9 @@ export const getToken = (req: IncomingMessage, res: ServerResponse) => { res.setHeader( 'set-cookie', serialize(csrfKey, key, { - expires: new Date(+new Date() + 24 * 60 * 60 * 1000), httpOnly: true, - path: '/', + sameSite: 'lax', + path: '/' }) ) } diff --git a/utils/Jwt.ts b/utils/Jwt.ts index d47e0c5..1c8b4ce 100644 --- a/utils/Jwt.ts +++ b/utils/Jwt.ts @@ -15,7 +15,8 @@ export function sign(payload: string | Record, options?: JWTSig export function verify(token: string): any | null { try { return jwt.verify(token, publicPem) - } catch { + } catch(e) { + console.log(e) return null } } diff --git a/utils/Query.ts b/utils/Query.ts index be930f1..d23d069 100644 --- a/utils/Query.ts +++ b/utils/Query.ts @@ -295,6 +295,11 @@ async function updateBotApplication(id: string, value: { webhook: string }) { return true } +async function updateOwner(id: string, owners: string[]): Promise { + await knex('bots').where({ id }).update({ owners: JSON.stringify(owners) }) + get.bot.clear(id) +} + async function resetBotToken(id: string, beforeToken: string) { const token = sign({ id }) const bot = await knex('bots').update({ token }).where({ id, token: beforeToken }) @@ -461,7 +466,8 @@ export const update = { resetBotToken, updateServer, Github, - bot: updateBot + bot: updateBot, + botOwners: updateOwner } export const put = { diff --git a/utils/Yup.ts b/utils/Yup.ts index 7e7698e..281d5f9 100644 --- a/utils/Yup.ts +++ b/utils/Yup.ts @@ -246,11 +246,6 @@ export const ManageBotSchema: Yup.SchemaOf = Yup.object({ .min(100, '봇 설명은 최소 100자여야합니다.') .max(1500, '봇 설명은 최대 1500자여야합니다.') .required('봇 설명은 필수 항목입니다.'), - // owners: Yup.array(Yup.string()) - // .min(1, '최소 한 명의 소유자는 입력해주세요.') - // .max(10, '소유자는 최대 10명까지만 가능합니다.') - // .unique('소유자 아이디는 중복될 수 없습니다.') - // .required('소유자는 필수 항목입니다.'), _csrf: Yup.string().required(), }) @@ -290,7 +285,7 @@ export interface DeveloperBot { _csrf: string } -export const ResetBotTokenSchema = Yup.object({ +export const ResetBotTokenSchema: Yup.SchemaOf = Yup.object({ token: Yup.string().required(), _csrf: Yup.string().required(), }) @@ -300,4 +295,20 @@ export interface ResetBotToken { _csrf: string } +export const EditBotOwnerSchema: Yup.SchemaOf = Yup.object({ + owners: Yup.array(Yup.string()) + .min(1, '최소 한 명의 소유자는 입력해주세요.') + .max(10, '소유자는 최대 10명까지만 가능합니다.') + .unique('소유자 아이디는 중복될 수 없습니다.') + .required('소유자는 필수 항목입니다.'), + _csrf: Yup.string().required(), + _captcha: Yup.string().required() +}) + +export interface EditBotOwner { + owners: string[] + _csrf: string + _captcha: string +} + export default Yup