import {Controller, Get} from '@overnightjs/core'; import type {NextFunction, Request, Response} from 'express'; import { API_URI, DASHBOARD_URI, GITHUB_OAUTH_CLIENT, GITHUB_OAUTH_ENABLED, GITHUB_OAUTH_SECRET, } from '../../app/constants.js'; import {prisma} from '../../database/prisma.js'; import {jwt} from '../../middleware/auth.js'; import {NtfyService} from '../../services/NtfyService.js'; import {UserService} from '../../services/UserService.js'; import {CatchAsync} from '../../utils/asyncHandler.js'; @Controller('github') export class Github { @Get('outbound') public sendToOutbound(req: Request, res: Response) { if (!GITHUB_OAUTH_ENABLED) { return res.status(404).json({error: 'GitHub OAuth is not configured'}); } const OAUTH_QS = new URLSearchParams({ client_id: GITHUB_OAUTH_CLIENT, redirect_uri: `${API_URI}/oauth/github/callback`, response_type: 'code', scope: 'user:email', }); return res.redirect(`https://github.com/login/oauth/authorize?${OAUTH_QS.toString()}`); } @Get('callback') @CatchAsync public async callback(req: Request, res: Response, _next: NextFunction) { if (!GITHUB_OAUTH_ENABLED) { return res.status(404).json({error: 'GitHub OAuth is not configured'}); } const {code} = req.query; if (!code || typeof code !== 'string') { return res.redirect(DASHBOARD_URI + '/auth/login?message=Invalid OAuth callback'); } const data = new URLSearchParams({ client_id: GITHUB_OAUTH_CLIENT, client_secret: GITHUB_OAUTH_SECRET, code: code as string, redirect_uri: `${API_URI}/oauth/github/callback`, }); const tokenResponse = await fetch('https://github.com/login/oauth/access_token', { method: 'POST', headers: {'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'application/json'}, body: data, }).then(res => res.json()); if (!tokenResponse.access_token || !tokenResponse.token_type) { return res.redirect(DASHBOARD_URI + '/auth/login?message=Failed to authenticate with GitHub'); } const emails = await fetch(`https://api.github.com/user/emails`, { headers: {Authorization: `${tokenResponse.token_type} ${tokenResponse.access_token}`}, }).then(res => res.json()); if (!Array.isArray(emails) || emails.length === 0) { return res.redirect(DASHBOARD_URI + '/auth/login?message=Failed to retrieve emails from GitHub'); } const primaryEmail = emails.find((e: {primary: boolean; email: string}) => e.primary); if (!primaryEmail || !primaryEmail.email || typeof primaryEmail.email !== 'string') { return res.redirect(DASHBOARD_URI + '/auth/login?message=Failed to retrieve primary email from GitHub'); } const email = primaryEmail.email; let user = await UserService.email(email); let isNewUser = false; if (!user) { user = await prisma.user.create({ data: { email, type: 'GITHUB_OAUTH', }, }); isNewUser = true; } if (user.type !== 'GITHUB_OAUTH') { return res.redirect(DASHBOARD_URI + '/auth/login?message=You used another form of authentication'); } // Send notification if this is a new user if (isNewUser) { await NtfyService.notifyUserOAuthSignup(user.email, user.id, 'GitHub'); } const token = jwt.sign(user.id); const cookie = UserService.cookieOptions(); res.cookie(UserService.COOKIE_NAME, token, cookie).redirect(DASHBOARD_URI); } }