feat: added owner transfer and edit

This commit is contained in:
wonderlandpark 2021-05-06 22:25:42 +09:00
parent d4b55eae60
commit 4e979831f3
6 changed files with 103 additions and 16 deletions

View File

@ -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

View File

@ -177,7 +177,20 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
</div>
<Button onClick={() => setAdminModal(true)} className='h-10 bg-red-500 hover:opacity-80 text-white lg:w-1/8'><i className='fas fa-user-cog' /> </Button>
<Modal full header='관리자 수정' isOpen={adminModal} dark={theme === 'dark'} onClose={() => setAdminModal(false)} closeIcon>
<Formik initialValues={{ owners: (bot.owners as User[]), id: '', _captcha: '' }} onSubmit={(v) => alert(JSON.stringify(v.owners.map(el => el.id)))}>
<Formik initialValues={{ owners: (bot.owners as User[]), id: '', _captcha: '' }} onSubmit={async (v) => {
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 }) => <Form>
<Message type='warning'>
@ -223,7 +236,7 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
</div>
</div>
<Captcha dark={theme === 'dark'} onVerify={(k) => setFieldValue('_captcha', k)} />
<Button disabled={!values._captcha} className='mt-2 bg-red-500 text-white hover:opacity-80' type='submit'><i className='fas fa-save text-sm' /> </Button>
<Button disabled={!values._captcha} className={`mt-2 bg-red-500 text-white ${!values._captcha ? 'opacity-80' : 'hover:opacity-80'}`} type='submit'><i className='fas fa-save text-sm' /> </Button>
</Form>
}
</Formik>
@ -237,11 +250,25 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
</div>
<Button onClick={() => setTransferModal(true)} className='h-10 bg-red-500 hover:opacity-80 text-white lg:w-1/8'><i className='fas fa-exchange-alt' /> </Button>
<Modal full header={`${bot.name} 소유권 이전하기`} isOpen={transferModal} dark={theme === 'dark'} onClose={() => setTransferModal(false)} closeIcon>
<Formik initialValues={{ ownerID: '', name: '', _captcha: '' }} onSubmit={(v) => alert(JSON.stringify(v))}>
<Formik initialValues={{ ownerID: '', name: '', _captcha: '' }} onSubmit={async (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 }) => <Form>
<Message type='warning'>
<p> .</p>
<h2 className='text-2xl font-bold'>!</h2>
<p> , .</p>
</Message>
<div className='py-4'>
<h2 className='text-md my-1'> ID를 .</h2>
@ -267,11 +294,11 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
<Modal full header={`${bot.name} 삭제하기`} isOpen={deleteModal} dark={theme === 'dark'} onClose={() => setDeleteModal(false)} closeIcon>
<Formik initialValues={{ name: '', _captcha: '', _csrf: csrfToken }} onSubmit={async (v) => {
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 }) => <Form>

View File

@ -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: '/'
})
)
}

View File

@ -15,7 +15,8 @@ export function sign(payload: string | Record<string, unknown>, options?: JWTSig
export function verify(token: string): any | null {
try {
return jwt.verify(token, publicPem)
} catch {
} catch(e) {
console.log(e)
return null
}
}

View File

@ -295,6 +295,11 @@ async function updateBotApplication(id: string, value: { webhook: string }) {
return true
}
async function updateOwner(id: string, owners: string[]): Promise<void> {
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 = {

View File

@ -246,11 +246,6 @@ export const ManageBotSchema: Yup.SchemaOf<ManageBot> = 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<ResetBotToken> = Yup.object({
token: Yup.string().required(),
_csrf: Yup.string().required(),
})
@ -300,4 +295,20 @@ export interface ResetBotToken {
_csrf: string
}
export const EditBotOwnerSchema: Yup.SchemaOf<EditBotOwner> = 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