mirror of
https://github.com/koreanbots/core.git
synced 2025-12-16 06:20:24 +00:00
feat: added owner transfer and edit
This commit is contained in:
parent
d4b55eae60
commit
4e979831f3
42
pages/api/v2/bots/[id]/owners.ts
Normal file
42
pages/api/v2/bots/[id]/owners.ts
Normal 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
|
||||||
@ -177,7 +177,20 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
|
|||||||
</div>
|
</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>
|
<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>
|
<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>
|
({ values, setFieldValue }) => <Form>
|
||||||
<Message type='warning'>
|
<Message type='warning'>
|
||||||
@ -223,7 +236,7 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Captcha dark={theme === 'dark'} onVerify={(k) => setFieldValue('_captcha', k)} />
|
<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>
|
</Form>
|
||||||
}
|
}
|
||||||
</Formik>
|
</Formik>
|
||||||
@ -237,11 +250,25 @@ const ManageBotPage:NextPage<ManageBotProps> = ({ bot, user, csrfToken, theme })
|
|||||||
</div>
|
</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>
|
<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>
|
<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>
|
({ values, setFieldValue }) => <Form>
|
||||||
<Message type='warning'>
|
<Message type='warning'>
|
||||||
<p>봇의 소유권을 이전하게 되면 봇의 소유자 권한을 이전하게 됩니다.</p>
|
<h2 className='text-2xl font-bold'>주의해주세요!</h2>
|
||||||
|
<p>봇의 소유권을 이전하게 되면 봇의 소유자 권한을 이전하게 되며, 본인을 포함한 모든 관리자가 해당 봇에 대한 권한을 잃게됩니다.</p>
|
||||||
</Message>
|
</Message>
|
||||||
<div className='py-4'>
|
<div className='py-4'>
|
||||||
<h2 className='text-md my-1'>이전하실 유저 ID를 입력해주세요.</h2>
|
<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>
|
<Modal full header={`${bot.name} 삭제하기`} isOpen={deleteModal} dark={theme === 'dark'} onClose={() => setDeleteModal(false)} closeIcon>
|
||||||
<Formik initialValues={{ name: '', _captcha: '', _csrf: csrfToken }} onSubmit={async (v) => {
|
<Formik initialValues={{ name: '', _captcha: '', _csrf: csrfToken }} onSubmit={async (v) => {
|
||||||
const res = await Fetch(`/bots/${bot.id}`, { method: 'DELETE', body: JSON.stringify(v) })
|
const res = await Fetch(`/bots/${bot.id}`, { method: 'DELETE', body: JSON.stringify(v) })
|
||||||
if(res.code !== 200) alert(res.message)
|
if(res.code === 200) {
|
||||||
else {
|
|
||||||
alert('성공적으로 삭제하였습니다.')
|
alert('성공적으로 삭제하였습니다.')
|
||||||
redirectTo(router, '/')
|
redirectTo(router, '/')
|
||||||
}
|
}
|
||||||
|
else alert(res.message)
|
||||||
}}>
|
}}>
|
||||||
{
|
{
|
||||||
({ values, setFieldValue }) => <Form>
|
({ values, setFieldValue }) => <Form>
|
||||||
|
|||||||
@ -21,9 +21,9 @@ export const getToken = (req: IncomingMessage, res: ServerResponse) => {
|
|||||||
res.setHeader(
|
res.setHeader(
|
||||||
'set-cookie',
|
'set-cookie',
|
||||||
serialize(csrfKey, key, {
|
serialize(csrfKey, key, {
|
||||||
expires: new Date(+new Date() + 24 * 60 * 60 * 1000),
|
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
path: '/',
|
sameSite: 'lax',
|
||||||
|
path: '/'
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,8 @@ export function sign(payload: string | Record<string, unknown>, options?: JWTSig
|
|||||||
export function verify(token: string): any | null {
|
export function verify(token: string): any | null {
|
||||||
try {
|
try {
|
||||||
return jwt.verify(token, publicPem)
|
return jwt.verify(token, publicPem)
|
||||||
} catch {
|
} catch(e) {
|
||||||
|
console.log(e)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -295,6 +295,11 @@ async function updateBotApplication(id: string, value: { webhook: string }) {
|
|||||||
return true
|
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) {
|
async function resetBotToken(id: string, beforeToken: string) {
|
||||||
const token = sign({ id })
|
const token = sign({ id })
|
||||||
const bot = await knex('bots').update({ token }).where({ id, token: beforeToken })
|
const bot = await knex('bots').update({ token }).where({ id, token: beforeToken })
|
||||||
@ -461,7 +466,8 @@ export const update = {
|
|||||||
resetBotToken,
|
resetBotToken,
|
||||||
updateServer,
|
updateServer,
|
||||||
Github,
|
Github,
|
||||||
bot: updateBot
|
bot: updateBot,
|
||||||
|
botOwners: updateOwner
|
||||||
}
|
}
|
||||||
|
|
||||||
export const put = {
|
export const put = {
|
||||||
|
|||||||
23
utils/Yup.ts
23
utils/Yup.ts
@ -246,11 +246,6 @@ export const ManageBotSchema: Yup.SchemaOf<ManageBot> = Yup.object({
|
|||||||
.min(100, '봇 설명은 최소 100자여야합니다.')
|
.min(100, '봇 설명은 최소 100자여야합니다.')
|
||||||
.max(1500, '봇 설명은 최대 1500자여야합니다.')
|
.max(1500, '봇 설명은 최대 1500자여야합니다.')
|
||||||
.required('봇 설명은 필수 항목입니다.'),
|
.required('봇 설명은 필수 항목입니다.'),
|
||||||
// owners: Yup.array(Yup.string())
|
|
||||||
// .min(1, '최소 한 명의 소유자는 입력해주세요.')
|
|
||||||
// .max(10, '소유자는 최대 10명까지만 가능합니다.')
|
|
||||||
// .unique('소유자 아이디는 중복될 수 없습니다.')
|
|
||||||
// .required('소유자는 필수 항목입니다.'),
|
|
||||||
_csrf: Yup.string().required(),
|
_csrf: Yup.string().required(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -290,7 +285,7 @@ export interface DeveloperBot {
|
|||||||
_csrf: string
|
_csrf: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ResetBotTokenSchema = Yup.object({
|
export const ResetBotTokenSchema: Yup.SchemaOf<ResetBotToken> = Yup.object({
|
||||||
token: Yup.string().required(),
|
token: Yup.string().required(),
|
||||||
_csrf: Yup.string().required(),
|
_csrf: Yup.string().required(),
|
||||||
})
|
})
|
||||||
@ -300,4 +295,20 @@ export interface ResetBotToken {
|
|||||||
_csrf: string
|
_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
|
export default Yup
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user