Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
76ded6c
commit e631a42
Showing
51 changed files
with
3,144 additions
and
944 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
import { promisify } from 'util'; | ||
import { Octokit } from 'probot'; | ||
import type { Router, Request, Response } from 'express'; | ||
import React from 'react'; | ||
import { renderToStaticMarkup } from 'react-dom/server'; | ||
import { sign, verify } from 'jsonwebtoken'; | ||
import * as githubAuth from '../auth/github'; | ||
import Layout from '../views/Layout'; | ||
|
||
if (!process.env.AUTH_SECRET_KEY) { | ||
throw new Error('Missing env variable: AUTH_SECRET_KEY'); | ||
} | ||
|
||
const AUTH_SECRET_KEY: string = process.env.AUTH_SECRET_KEY; | ||
|
||
const signPromisified: any = promisify(sign); | ||
const verifyPromisified: any = promisify(verify); | ||
|
||
const secure = | ||
!!process.env.SECURE_COOKIE && process.env.SECURE_COOKIE !== 'false'; | ||
|
||
const createRedirectUri = (req: Request, strategy: string): string => { | ||
const host = `http${secure ? 's' : ''}://${req.hostname}${ | ||
req.hostname === 'localhost' ? `:${process.env.PORT}` : '' | ||
}`; | ||
return `${host}/app/${strategy}/login-response`; | ||
}; | ||
|
||
interface AuthInfo { | ||
id: number; | ||
login: string; | ||
accessToken: string; | ||
time: number; | ||
} | ||
|
||
const readAuthCookie = ( | ||
req: Request, | ||
strategy: string, | ||
): undefined | Promise<undefined | AuthInfo> => { | ||
const cookie = req.cookies[`auth_${strategy}`]; | ||
if (!cookie) return; | ||
|
||
return verifyPromisified(cookie, AUTH_SECRET_KEY, { | ||
algorithm: 'HS512', | ||
audience: req.headers['user-agent'], | ||
}); | ||
}; | ||
|
||
const getAuthInfoFromCookie = async ( | ||
req: Request, | ||
res: Response, | ||
): Promise<undefined | AuthInfo> => { | ||
const strategy = 'gh'; // req.params.strategy | ||
const authInfo = await readAuthCookie(req, strategy); | ||
|
||
if (authInfo?.id) { | ||
return authInfo; | ||
} | ||
|
||
res.clearCookie(`auth_${strategy}`); | ||
return undefined; | ||
}; | ||
|
||
export const getUser = async ( | ||
req: Request, | ||
res: Response, | ||
): Promise<{ authInfo: AuthInfo; api: Octokit } | null> => { | ||
const authInfo = await getAuthInfoFromCookie(req, res); | ||
if (!authInfo) { | ||
res.redirect('/app/gh/login'); | ||
return null; | ||
} | ||
|
||
return { | ||
authInfo, | ||
api: new Octokit({ auth: `token ${authInfo.accessToken}` }), | ||
}; | ||
}; | ||
|
||
export default function auth(router: Router): void { | ||
router.get('/gh/login', async (req: Request, res: Response) => { | ||
if (await getAuthInfoFromCookie(req, res)) { | ||
return res.redirect('/app/gh'); | ||
} | ||
|
||
// const state = await randomHex(8); | ||
// res.cookie(`auth_${strategy}_${state}`, strategy, { | ||
// maxAge: 10 * 60 * 1000, | ||
// httpOnly: true, | ||
// secure, | ||
// }); | ||
|
||
const redirectUri = githubAuth.oauth2.authorizationCode.authorizeURL({ | ||
redirect_uri: createRedirectUri(req, 'gh'), | ||
scope: 'read:user,repo', | ||
// state, | ||
// grant_type: options.grantType, | ||
// access_type: options.accessType, | ||
// login_hint: req.query.loginHint, | ||
// include_granted_scopes: options.includeGrantedScopes, | ||
}); | ||
|
||
// console.log(redirectUri); | ||
|
||
res.redirect(redirectUri); | ||
}); | ||
|
||
router.get('/gh/login-response', async (req, res) => { | ||
if (req.query.error) { | ||
res.send(req.query.error_description); | ||
return; | ||
} | ||
|
||
const strategy = 'gh'; | ||
const code: string = req.query.code as string; | ||
// const state = req.query.state; | ||
// const cookieName = `auth_${strategy}_${state}`; | ||
// const cookie = req.cookies && req.cookies[cookieName]; | ||
// if (!cookie) { | ||
// // res.redirect(`/${strategy}/login`); | ||
// res.send( | ||
// '<html><body>No cookie for this state. <a href="/app/gh/login">Retry ?</a></body></html>', | ||
// ); | ||
// return; | ||
// } | ||
// res.clearCookie(cookieName); | ||
|
||
const result = await githubAuth.oauth2.authorizationCode.getToken({ | ||
code, | ||
redirect_uri: createRedirectUri(req, strategy), | ||
}); | ||
|
||
if (!result) { | ||
res.send( | ||
renderToStaticMarkup( | ||
<Layout> | ||
<div> | ||
Could not get access token. <a href="/app/gh/login">Retry ?</a> | ||
</div> | ||
</Layout>, | ||
), | ||
); | ||
return; | ||
} | ||
|
||
const accessToken = result.access_token; | ||
const octokit = new Octokit({ auth: `token ${accessToken}` }); | ||
const user = await octokit.users.getAuthenticated({}); | ||
const id = user.data.id; | ||
const login = user.data.login; | ||
|
||
const authInfo: AuthInfo = { id, login, accessToken, time: Date.now() }; | ||
console.log({ authInfo }); | ||
const token = await signPromisified(authInfo, AUTH_SECRET_KEY, { | ||
algorithm: 'HS512', | ||
audience: req.headers['user-agent'], | ||
expiresIn: '10 days', | ||
}); | ||
|
||
res.cookie(`auth_${strategy}`, token, { | ||
httpOnly: true, | ||
secure, | ||
}); | ||
|
||
res.redirect('/app/gh'); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import type { Router } from 'express'; | ||
import React from 'react'; | ||
import { renderToStaticMarkup } from 'react-dom/server'; | ||
import { GitHubAPI } from 'probot/lib/github'; | ||
import type { MongoStores } from '../mongo'; | ||
import Layout from '../views/Layout'; | ||
import { getUser } from './auth'; | ||
|
||
export default function home( | ||
router: Router, | ||
api: GitHubAPI, | ||
mongoStores: MongoStores, | ||
): void { | ||
router.get('/gh', async (req, res) => { | ||
const user = await getUser(req, res); | ||
if (!user) return; | ||
|
||
const orgs = await user.api.orgs.listForAuthenticatedUser(); | ||
|
||
res.send( | ||
renderToStaticMarkup( | ||
<Layout> | ||
<div> | ||
<h1>{process.env.REVIEWFLOW_NAME}</h1> | ||
<div style={{ display: 'flex' }}> | ||
<div style={{ flexGrow: 1 }}> | ||
<h4>Choose your account</h4> | ||
<ul> | ||
<li> | ||
<a href="/app/gh/user">{user.authInfo.login}</a> | ||
</li> | ||
{orgs.data.map((org) => ( | ||
<li key={org.id}> | ||
<a href={`/app/gh/org/${org.login}`}>{org.login}</a> | ||
</li> | ||
))} | ||
</ul> | ||
</div> | ||
</div> | ||
</div> | ||
</Layout>, | ||
), | ||
); | ||
}); | ||
} |
Oops, something went wrong.