Skip to content

Commit 56f1ef3

Browse files
Merge pull request #8 from zunalita/main
sync
2 parents 7b508b0 + 82e8e2e commit 56f1ef3

File tree

6 files changed

+117
-83
lines changed

6 files changed

+117
-83
lines changed

api-map.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
[
2+
"lib/cors",
3+
"lib/env",
4+
"lib/github",
25
"oauth",
36
"revoke"
47
]

api/lib/cors.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export function setCors(res) {
2+
res.setHeader("Access-Control-Allow-Origin", "https://zunalita.github.io");
3+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
4+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
5+
}
6+
7+
export function handleOptions(req, res) {
8+
if (req.method === "OPTIONS") {
9+
res.status(204).end();
10+
return true;
11+
}
12+
return false;
13+
}

api/lib/env.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export function requireClientCreds() {
2+
const client_id = process.env.GITHUB_CLIENT_ID;
3+
const client_secret = process.env.GITHUB_CLIENT_SECRET;
4+
5+
if (!client_id || !client_secret) {
6+
const err = new Error('Server misconfigured: missing GITHUB_CLIENT_ID or GITHUB_CLIENT_SECRET');
7+
err.code = 'MISSING_GITHUB_CREDS';
8+
throw err;
9+
}
10+
11+
return { client_id, client_secret };
12+
}

api/lib/github.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
async function fetchJson(url, options) {
2+
const res = await fetch(url, options);
3+
const text = await res.text();
4+
try {
5+
return { ok: res.ok, status: res.status, body: JSON.parse(text) };
6+
} catch (e) {
7+
return { ok: res.ok, status: res.status, body: text };
8+
}
9+
}
10+
11+
export async function exchangeCodeForToken(code, client_id, client_secret) {
12+
const { ok, status, body } = await fetchJson("https://github.com/login/oauth/access_token", {
13+
method: "POST",
14+
headers: { "Content-Type": "application/json", "Accept": "application/json" },
15+
body: JSON.stringify({ client_id, client_secret, code }),
16+
});
17+
18+
if (!ok) {
19+
throw new Error(`GitHub token exchange failed: ${status} ${JSON.stringify(body)}`);
20+
}
21+
22+
if (body && body.error) {
23+
throw new Error(body.error_description || body.error);
24+
}
25+
26+
return body && body.access_token;
27+
}
28+
29+
export async function revokeToken(token, client_id, client_secret) {
30+
const { ok, status, body } = await fetchJson(
31+
`https://api.github.com/applications/${client_id}/token`,
32+
{
33+
method: "DELETE",
34+
headers: {
35+
"Content-Type": "application/json",
36+
"Accept": "application/json",
37+
"Authorization": `Basic ${Buffer.from(`${client_id}:${client_secret}`).toString("base64")}`,
38+
},
39+
body: JSON.stringify({ access_token: token }),
40+
}
41+
);
42+
43+
if (!ok) {
44+
throw new Error(`GitHub token revocation failed: ${status} ${JSON.stringify(body)}`);
45+
}
46+
47+
return true;
48+
}

api/oauth.js

Lines changed: 19 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,33 @@
1-
export default async function handler(req, res) {
2-
res.setHeader("Access-Control-Allow-Origin", "https://zunalita.github.io");
3-
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
4-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1+
import { setCors, handleOptions } from './lib/cors.js';
2+
import { requireClientCreds } from './lib/env.js';
3+
import { exchangeCodeForToken } from './lib/github.js';
54

6-
if (req.method === "OPTIONS") {
7-
return res.status(204).end();
8-
}
5+
export default async function handler(req, res) {
6+
setCors(res);
7+
if (handleOptions(req, res)) return;
98

10-
if (req.method !== "POST") {
11-
return res.status(405).json({ error: "Method not allowed" });
9+
if (req.method !== 'POST') {
10+
return res.status(405).json({ error: 'Method not allowed' });
1211
}
1312

14-
const { code } = req.body;
15-
13+
const { code } = req.body || {};
1614
if (!code) {
17-
return res.status(400).json({ error: "Missing code" });
15+
return res.status(400).json({ error: 'Missing code' });
1816
}
1917

20-
const client_id = process.env.GITHUB_CLIENT_ID;
21-
const client_secret = process.env.GITHUB_CLIENT_SECRET;
22-
23-
if (!client_id || !client_secret) {
24-
return res.status(500).json({ error: "Server misconfigured" });
18+
let client_id, client_secret;
19+
try {
20+
({ client_id, client_secret } = requireClientCreds());
21+
} catch (err) {
22+
console.error(err);
23+
return res.status(500).json({ error: 'Server misconfigured' });
2524
}
2625

2726
try {
28-
const tokenRes = await fetch("https://github.com/login/oauth/access_token", {
29-
method: "POST",
30-
headers: { "Content-Type": "application/json", "Accept": "application/json" },
31-
body: JSON.stringify({
32-
client_id,
33-
client_secret,
34-
code,
35-
}),
36-
});
37-
38-
if (!tokenRes.ok) {
39-
const errText = await tokenRes.text();
40-
throw new Error(`GitHub responded: ${errText}`);
41-
}
42-
43-
const data = await tokenRes.json();
44-
45-
if (data.error) {
46-
return res.status(400).json({ error: data.error_description || data.error });
47-
}
48-
49-
const access_token = data.access_token;
50-
51-
res.status(200).json({ token: access_token });
27+
const access_token = await exchangeCodeForToken(code, client_id, client_secret);
28+
return res.status(200).json({ token: access_token });
5229
} catch (err) {
5330
console.error(err);
54-
res.status(500).json({ error: "OAuth exchange failed" });
31+
return res.status(500).json({ error: 'OAuth exchange failed' });
5532
}
5633
}

api/revoke.js

Lines changed: 22 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,40 @@
1+
import { setCors, handleOptions } from './lib/cors.js';
2+
import { requireClientCreds } from './lib/env.js';
3+
import { revokeToken } from './lib/github.js';
4+
15
// Revoking oauth user tokens for more security at Zunalita!
26
export default async function handler(req, res) {
3-
// Set CORS headers
4-
res.setHeader("Access-Control-Allow-Origin", "https://zunalita.github.io");
5-
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
6-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
7-
8-
if (req.method === "OPTIONS") {
9-
return res.status(204).end();
10-
}
7+
setCors(res);
8+
if (handleOptions(req, res)) return;
119

12-
if (req.method !== "POST") {
13-
return res.status(405).json({ error: "method not allowed" });
10+
if (req.method !== 'POST') {
11+
return res.status(405).json({ error: 'method not allowed' });
1412
}
1513

16-
const { token } = req.body;
17-
14+
const { token } = req.body || {};
1815
if (!token) {
19-
return res.status(400).json({ error: "missing token" });
16+
return res.status(400).json({ error: 'missing token' });
2017
}
2118

22-
// --- Prefix check (quick validation) ---
23-
const validPrefixes = ["ghp_", "gho_", "ghu_", "ghs_", "ghr_"];
19+
// quick format validation
20+
const validPrefixes = ['ghp_', 'gho_', 'ghu_', 'ghs_', 'ghr_'];
2421
if (!validPrefixes.some(prefix => token.startsWith(prefix))) {
25-
return res.status(400).json({ error: "invalid token format" });
22+
return res.status(400).json({ error: 'invalid token format' });
2623
}
2724

28-
const client_id = process.env.GITHUB_CLIENT_ID;
29-
const client_secret = process.env.GITHUB_CLIENT_SECRET;
30-
31-
if (!client_id || !client_secret) {
32-
return res.status(500).json({ error: "server misconfigured" });
25+
let client_id, client_secret;
26+
try {
27+
({ client_id, client_secret } = requireClientCreds());
28+
} catch (err) {
29+
console.error(err);
30+
return res.status(500).json({ error: 'server misconfigured' });
3331
}
3432

3533
try {
36-
const tokenRevocationRes = await fetch(
37-
`https://api.github.com/applications/${client_id}/token`,
38-
{
39-
method: "DELETE",
40-
headers: {
41-
"Content-Type": "application/json",
42-
"Accept": "application/json",
43-
"Authorization": `Basic ${Buffer.from(`${client_id}:${client_secret}`).toString("base64")}`,
44-
},
45-
body: JSON.stringify({ access_token: token }),
46-
}
47-
);
48-
49-
if (!tokenRevocationRes.ok) {
50-
const errText = await tokenRevocationRes.text();
51-
throw new Error(`gitHub responded with an error during token revocation: ${errText}`);
52-
}
53-
54-
res.status(200).json({ message: "token revoked successfully" });
34+
await revokeToken(token, client_id, client_secret);
35+
return res.status(200).json({ message: 'token revoked successfully' });
5536
} catch (err) {
5637
console.error(err);
57-
res.status(500).json({ error: "token revocation failed" });
38+
return res.status(500).json({ error: 'token revocation failed' });
5839
}
5940
}

0 commit comments

Comments
 (0)