diff --git a/pages/api/auth/github/callback.ts b/pages/api/auth/github/callback.ts new file mode 100644 index 0000000..44717b8 --- /dev/null +++ b/pages/api/auth/github/callback.ts @@ -0,0 +1,48 @@ +import { NextApiRequest } from 'next' +import fetch from 'node-fetch' + +import { GithubTokenInfo } from '@types' +import { SpecialEndPoints } from '@utils/Constants' +import { OauthCallbackSchema } from '@utils/Yup' +import ResponseWrapper from '@utils/ResponseWrapper' +import { get, update } from '@utils/Query' +import RequestHandler from '@utils/RequestHandler' + +const Callback = RequestHandler().get(async (req: ApiRequest, res) => { + const validate = await OauthCallbackSchema.validate(req.query) + .then(r => r) + .catch(e => { + ResponseWrapper(res, { code: 400, errors: e.errors }) + return null + }) + + if (!validate) return + + const user = await get.Authorization(req.cookies.token) + if (!user) return ResponseWrapper(res, { code: 401 }) + const token: GithubTokenInfo = await fetch(SpecialEndPoints.Github.Token(process.env.GITHUB_CLIENT_ID, process.env.GITHUB_CLIENT_SECRET,req.query.code), { + method: 'POST', + headers: { + Accept: 'application/json' + }, + }).then(r => r.json()) + if (token.error) return ResponseWrapper(res, { code: 400, errors: ['올바르지 않은 코드입니다.'] }) + + const github: { login: string } = await fetch(SpecialEndPoints.Github.Me, { + headers: { + Authorization: `token ${token.access_token}` + } + }).then(r => r.json()) + const result = await update.Github(user, github.login) + if(result === 0) return ResponseWrapper(res, { code: 400, message: '이미 등록되어있는 깃허브 계정입니다.' }) + get.user.clear(user) + res.redirect(301, '/panel') +}) + +interface ApiRequest extends NextApiRequest { + query: { + code: string + } +} + +export default Callback diff --git a/pages/api/auth/github/index.ts b/pages/api/auth/github/index.ts new file mode 100644 index 0000000..96cda63 --- /dev/null +++ b/pages/api/auth/github/index.ts @@ -0,0 +1,30 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { generateOauthURL } from '@utils/Tools' +import RequestHandler from '@utils/RequestHandler' +import ResponseWrapper from '@utils/ResponseWrapper' +import { get, update } from '@utils/Query' +import { checkToken } from '@utils/Csrf' + +const Github = RequestHandler().get(async (_req: NextApiRequest, res: NextApiResponse) => { + res.redirect( + 301, + generateOauthURL('github', process.env.GITHUB_CLIENT_ID) + ) +}) + .delete(async (req: DeleteApiRequest, res) => { + const user = await get.Authorization(req.cookies.token) + if (!user) return ResponseWrapper(res, { code: 401 }) + const csrfValidated = checkToken(req, res, req.body._csrf) + if(!csrfValidated) return + await update.Github(user, null) + get.user.clear(user) + return ResponseWrapper(res, { code: 200 }) + }) + +interface DeleteApiRequest extends NextApiRequest { + body: { + _csrf: string + } +} + +export default Github diff --git a/pages/panel.tsx b/pages/panel.tsx index 8815f8f..9f3e74c 100644 --- a/pages/panel.tsx +++ b/pages/panel.tsx @@ -6,15 +6,17 @@ import { useRouter } from 'next/router' import { get } from '@utils/Query' import { parseCookie, redirectTo } from '@utils/Tools' import { Bot, SubmittedBot, User } from '@types' -import BotCard from '@components/BotCard' -import SubmittedBotCard from '@components/SubmittedBotCard' -import Button from '@components/Button' +import Fetch from '@utils/Fetch' +import { getToken } from '@utils/Csrf' const Container = dynamic(() => import('@components/Container')) const SEO = dynamic(() => import('@components/SEO')) const ResponsiveGrid = dynamic(() => import('@components/ResponsiveGrid')) +const Button = dynamic(() => import('@components/Button')) +const BotCard = dynamic(() => import('@components/BotCard')) +const SubmittedBotCard = dynamic(() => import('@components/SubmittedBotCard')) -const Panel:NextPage = ({ logged, user, submits }) => { +const Panel:NextPage = ({ logged, user, submits, csrfToken }) => { const router = useRouter() const [ submitLimit, setSubmitLimit ] = useState(8) function toLogin() { @@ -28,6 +30,18 @@ const Panel:NextPage = ({ logged, user, submits }) => { return

관리 패널

+

깃허브 계정 연동

+

나의 봇

{ @@ -68,13 +82,14 @@ export const getServerSideProps = async (ctx: NextPageContext) => { const user = await get.Authorization(parsed?.token) || '' const submits = await get.botSubmits.load(user) - return { props: { logged: !!user, user: await get.user.load(user), submits } } + return { props: { logged: !!user, user: await get.user.load(user), submits, csrfToken: getToken(ctx.req, ctx.res) } } } interface PanelProps { logged: boolean user: User submits: SubmittedBot[] + csrfToken: string } export default Panel \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index a698f6a..d145bba 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -26,7 +26,8 @@ module.exports = { 'discord-dark-hover': '#383f48', 'discord-black': '#23272A', 'discord-pink': '#FF73FA', - 'very-black': '#1b1e23' + 'very-black': '#1b1e23', + 'github-black': '#24292e' } }, minHeight: { diff --git a/types/index.ts b/types/index.ts index 5640d7a..35e41fd 100644 --- a/types/index.ts +++ b/types/index.ts @@ -115,6 +115,13 @@ export interface DiscordTokenInfo { error?: string } +export interface GithubTokenInfo { + access_token?: string + scope?: string + token_type?: string + error?: string +} + export interface TokenRegister { id: string access_token: string diff --git a/utils/Constants.ts b/utils/Constants.ts index 303d356..fe809a0 100644 --- a/utils/Constants.ts +++ b/utils/Constants.ts @@ -180,12 +180,20 @@ export const KoreanbotsEndPoints = { logout: '/api/auth/discord/logout' } +export const SpecialEndPoints = { + Github: { + Token: (clientID: string, clientSecret: string, code: string) => `https://github.com/login/oauth/access_token?client_id=${clientID}&client_secret=${clientSecret}&code=${code}`, + Me: 'https://api.github.com/user' + } +} + export const GlobalRatelimitIgnore = [ '/api/image/discord/avatars/' ] export const Oauth = { - discord: (clientID: string, scope: string) => `https://discord.com/oauth2/authorize?client_id=${clientID}&scope=${scope}&permissions=0&response_type=code&redirect_uri=${process.env.KOREANBOTS_URL}/api/auth/discord/callback&prompt=none` + discord: (clientID: string, scope: string) => `https://discord.com/oauth2/authorize?client_id=${clientID}&scope=${scope}&permissions=0&response_type=code&redirect_uri=${process.env.KOREANBOTS_URL}/api/auth/discord/callback&prompt=none`, + github: (clientID: string) => `https://github.com/login/oauth/authorize?client_id=${clientID}&redirect_uri=${process.env.KOREANBOTS_URL}/api/auth/github/callback` } export const git = { 'github.com': { icon: 'github', text: 'Github' }, 'gitlab.com': { icon: 'gitlab', text: 'Gitlab' }} diff --git a/utils/Fetch.ts b/utils/Fetch.ts index ba14e4e..2e88eca 100644 --- a/utils/Fetch.ts +++ b/utils/Fetch.ts @@ -1,8 +1,8 @@ import { ResponseProps } from '@types' import { KoreanbotsEndPoints } from './Constants' -const Fetch = async (endpoint: string, options?: RequestInit): Promise> => { - const url = KoreanbotsEndPoints.baseAPI + (endpoint.startsWith('/') ? endpoint : '/' + endpoint) +const Fetch = async (endpoint: string, options?: RequestInit, rawEndpoint=false): Promise> => { + const url = (rawEndpoint ? '' : KoreanbotsEndPoints.baseAPI) + (endpoint.startsWith('/') ? endpoint : '/' + endpoint) const res = await fetch(url, { method: 'GET', diff --git a/utils/Query.ts b/utils/Query.ts index 16317e1..1eeb41f 100644 --- a/utils/Query.ts +++ b/utils/Query.ts @@ -250,6 +250,13 @@ async function resetBotToken(id: string, beforeToken: string) { return token } +async function Github(id: string, github: string) { + const user = await knex('users').where({ github }).whereNot({ id }) + if(github && user.length !== 0) return 0 + await knex('users').update({ github }).where({ id }) + return 1 +} + async function getImage(url: string) { const res = await fetch(url) if(!res.ok) return null @@ -373,7 +380,8 @@ export const update = { assignToken, updateBotApplication, resetBotToken, - updateServer + updateServer, + Github } export const put = { diff --git a/utils/Tools.ts b/utils/Tools.ts index 72ae899..848fc27 100644 --- a/utils/Tools.ts +++ b/utils/Tools.ts @@ -75,7 +75,7 @@ export function checkBrowser(): string { return M.join(' ') } -export function generateOauthURL(provider: 'discord', clientID: string, scope: string) { +export function generateOauthURL(provider: 'discord'|'github', clientID: string, scope?: string) { return Oauth[provider](clientID, scope) }