feat: added github

This commit is contained in:
wonderlandpark 2021-03-23 22:53:11 +09:00
parent 85071cac6d
commit 0b0d5e4dba
9 changed files with 128 additions and 11 deletions

View File

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

View File

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

View File

@ -6,15 +6,17 @@ import { useRouter } from 'next/router'
import { get } from '@utils/Query' import { get } from '@utils/Query'
import { parseCookie, redirectTo } from '@utils/Tools' import { parseCookie, redirectTo } from '@utils/Tools'
import { Bot, SubmittedBot, User } from '@types' import { Bot, SubmittedBot, User } from '@types'
import BotCard from '@components/BotCard' import Fetch from '@utils/Fetch'
import SubmittedBotCard from '@components/SubmittedBotCard' import { getToken } from '@utils/Csrf'
import Button from '@components/Button'
const Container = dynamic(() => import('@components/Container')) const Container = dynamic(() => import('@components/Container'))
const SEO = dynamic(() => import('@components/SEO')) const SEO = dynamic(() => import('@components/SEO'))
const ResponsiveGrid = dynamic(() => import('@components/ResponsiveGrid')) 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<PanelProps> = ({ logged, user, submits }) => { const Panel:NextPage<PanelProps> = ({ logged, user, submits, csrfToken }) => {
const router = useRouter() const router = useRouter()
const [ submitLimit, setSubmitLimit ] = useState(8) const [ submitLimit, setSubmitLimit ] = useState(8)
function toLogin() { function toLogin() {
@ -28,6 +30,18 @@ const Panel:NextPage<PanelProps> = ({ logged, user, submits }) => {
return <Container paddingTop className='pt-5 pb-10'> return <Container paddingTop className='pt-5 pb-10'>
<SEO title='관리 패널' /> <SEO title='관리 패널' />
<h1 className='text-4xl font-bold'> </h1> <h1 className='text-4xl font-bold'> </h1>
<h2 className='text-3xl font-bold my-4'> </h2>
<Button className='bg-github-black hover:opacity-80' onClick={user.github ? async () => {
await Fetch('/api/auth/github', {
method: 'DELETE',
body: JSON.stringify({
_csrf: csrfToken
})
}, true)
router.reload()
} : null} href={user.github ? null : '/api/auth/github'}>
<i className='fab fa-github' /> {user.github ? '취소' : ''}
</Button>
<div className='mt-6'> <div className='mt-6'>
<h2 className='text-3xl font-bold'> </h2> <h2 className='text-3xl font-bold'> </h2>
{ {
@ -68,13 +82,14 @@ export const getServerSideProps = async (ctx: NextPageContext) => {
const user = await get.Authorization(parsed?.token) || '' const user = await get.Authorization(parsed?.token) || ''
const submits = await get.botSubmits.load(user) 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 { interface PanelProps {
logged: boolean logged: boolean
user: User user: User
submits: SubmittedBot[] submits: SubmittedBot[]
csrfToken: string
} }
export default Panel export default Panel

View File

@ -26,7 +26,8 @@ module.exports = {
'discord-dark-hover': '#383f48', 'discord-dark-hover': '#383f48',
'discord-black': '#23272A', 'discord-black': '#23272A',
'discord-pink': '#FF73FA', 'discord-pink': '#FF73FA',
'very-black': '#1b1e23' 'very-black': '#1b1e23',
'github-black': '#24292e'
} }
}, },
minHeight: { minHeight: {

View File

@ -115,6 +115,13 @@ export interface DiscordTokenInfo {
error?: string error?: string
} }
export interface GithubTokenInfo {
access_token?: string
scope?: string
token_type?: string
error?: string
}
export interface TokenRegister { export interface TokenRegister {
id: string id: string
access_token: string access_token: string

View File

@ -180,12 +180,20 @@ export const KoreanbotsEndPoints = {
logout: '/api/auth/discord/logout' 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 = [ export const GlobalRatelimitIgnore = [
'/api/image/discord/avatars/' '/api/image/discord/avatars/'
] ]
export const Oauth = { 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' }} export const git = { 'github.com': { icon: 'github', text: 'Github' }, 'gitlab.com': { icon: 'gitlab', text: 'Gitlab' }}

View File

@ -1,8 +1,8 @@
import { ResponseProps } from '@types' import { ResponseProps } from '@types'
import { KoreanbotsEndPoints } from './Constants' import { KoreanbotsEndPoints } from './Constants'
const Fetch = async <T>(endpoint: string, options?: RequestInit): Promise<ResponseProps<T>> => { const Fetch = async <T>(endpoint: string, options?: RequestInit, rawEndpoint=false): Promise<ResponseProps<T>> => {
const url = KoreanbotsEndPoints.baseAPI + (endpoint.startsWith('/') ? endpoint : '/' + endpoint) const url = (rawEndpoint ? '' : KoreanbotsEndPoints.baseAPI) + (endpoint.startsWith('/') ? endpoint : '/' + endpoint)
const res = await fetch(url, { const res = await fetch(url, {
method: 'GET', method: 'GET',

View File

@ -250,6 +250,13 @@ async function resetBotToken(id: string, beforeToken: string) {
return token 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) { async function getImage(url: string) {
const res = await fetch(url) const res = await fetch(url)
if(!res.ok) return null if(!res.ok) return null
@ -373,7 +380,8 @@ export const update = {
assignToken, assignToken,
updateBotApplication, updateBotApplication,
resetBotToken, resetBotToken,
updateServer updateServer,
Github
} }
export const put = { export const put = {

View File

@ -75,7 +75,7 @@ export function checkBrowser(): string {
return M.join(' ') 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) return Oauth[provider](clientID, scope)
} }