From e2a14fc4d1775455831c95fefe73216b8328bf6a Mon Sep 17 00:00:00 2001 From: martin-mfg <2026226+martin-mfg@users.noreply.github.com> Date: Tue, 2 Sep 2025 10:57:15 +0200 Subject: [PATCH] add login functionality --- .../output/functions/api.func/router.js | 4 + .../output/functions/api/login.func | 1 + api-renamed/login.js | 18 ++ src/common/database.js | 165 ++++++++++++++++-- src/users.js | 104 +++++++++++ 5 files changed, 281 insertions(+), 11 deletions(-) create mode 120000 _dot_vercel_copy/output/functions/api/login.func create mode 100644 api-renamed/login.js create mode 100644 src/users.js diff --git a/_dot_vercel_copy/output/functions/api.func/router.js b/_dot_vercel_copy/output/functions/api.func/router.js index 16c1ed8a..4999d7b3 100644 --- a/_dot_vercel_copy/output/functions/api.func/router.js +++ b/_dot_vercel_copy/output/functions/api.func/router.js @@ -6,6 +6,7 @@ import { default as wakatime } from "./api-renamed/wakatime.js"; import { default as repeatRecent } from "./api-renamed/repeat-recent.js"; import { default as patInfo } from "./api-renamed/status/pat-info.js"; import { default as statusUp } from "./api-renamed/status/up.js"; +import { default as login } from "./api-renamed/login.js"; export default async (req, res) => { // remaining code expects express.js-like request and response objects @@ -47,6 +48,9 @@ export default async (req, res) => { case "/api/status/up": statusUp(req, res); break; + case "/api/login": + login(req, res); + break; default: res.statusCode = 404; res.end("Not Found"); diff --git a/_dot_vercel_copy/output/functions/api/login.func b/_dot_vercel_copy/output/functions/api/login.func new file mode 120000 index 00000000..2e79e533 --- /dev/null +++ b/_dot_vercel_copy/output/functions/api/login.func @@ -0,0 +1 @@ +../api.func \ No newline at end of file diff --git a/api-renamed/login.js b/api-renamed/login.js new file mode 100644 index 00000000..5344a917 --- /dev/null +++ b/api-renamed/login.js @@ -0,0 +1,18 @@ +import { logger } from "../src/common/utils.js"; +import { authenticate } from "../src/users.js"; + +/** + * @param {any} req The request. + * @param {any} res The response. + */ +export default async (req, res) => { + const { code, private_access, user_key } = req.query; + res.setHeader("Content-Type", "application/json"); + try { + await authenticate(code, private_access === "true", user_key); + } catch (err) { + logger.error(err); + res.send("Something went wrong: " + err.message); + } + res.send("ok"); +}; diff --git a/src/common/database.js b/src/common/database.js index 841645eb..b38c3089 100644 --- a/src/common/database.js +++ b/src/common/database.js @@ -7,6 +7,33 @@ const pool = process.env.POSTGRES_URL }) : null; +/* + * Creates all required tables if they do not exist + */ +async function createAllTables() { + if (!pool) { + return; + } + + await pool.query(` + CREATE TABLE IF NOT EXISTS requests ( + request TEXT PRIMARY KEY, + requested_at TIMESTAMP NOT NULL DEFAULT now(), + user_requested_at TIMESTAMP NOT NULL DEFAULT now() + ); + CREATE TABLE IF NOT EXISTS users ( + user_id TEXT PRIMARY KEY, + access_token TEXT NOT NULL, + user_key TEXT, + private_access BOOLEAN NOT NULL DEFAULT false + ); + -- CREATE TABLE IF NOT EXISTS code_key_map ( + -- code TEXT PRIMARY KEY, + -- user_key TEXT NOT NULL + -- ); + `); +} + /** * Stores or updates a request in the database. */ @@ -35,14 +62,7 @@ export async function storeRequest(req) { } catch (err) { // Check for undefined_table error (SQLSTATE 42P01) if (err.code === "42P01") { - const createTableQuery = ` - CREATE TABLE IF NOT EXISTS requests ( - request TEXT PRIMARY KEY, - requested_at TIMESTAMP NOT NULL DEFAULT now(), - user_requested_at TIMESTAMP NOT NULL DEFAULT now() - ) - `; - await pool.query(createTableQuery); + await createAllTables(); // Retry the insert after creating the table await pool.query(insertQuery, [req.url]); } else { @@ -63,8 +83,18 @@ export async function deleteOldRequests() { DELETE FROM requests WHERE user_requested_at < NOW() - INTERVAL '8 days' `; - const result = await pool.query(deleteQuery); - console.log(`Deleted ${result.rowCount} old requests.`); + let result; + try { + result = await pool.query(deleteQuery); + } catch (err) { + if (err.code === "42P01") { + await createAllTables(); + result = await pool.query(deleteQuery); + } else { + throw err; + } + console.log(`Deleted ${result.rowCount} old requests.`); + } } /** @@ -83,6 +113,119 @@ export async function getRecentRequests() { AND requested_at < NOW() - INTERVAL '11 hours' ORDER BY requested_at ASC `; - const { rows } = await pool.query(query); + let rows; + try { + ({ rows } = await pool.query(query)); + } catch (err) { + if (err.code === "42P01") { + await createAllTables(); + ({ rows } = await pool.query(query)); + } else { + throw err; + } + } return rows.map((row) => row.request); } + +/** + * Inserts or updates a user in the database. + * @param {string} userId GitHub userId (login name) + * @param {string} accessToken GitHub access token + * @param {string|null} userKey Optional user key + * @param {boolean} privateAccess Whether private access was requested + */ +export async function storeUser(userId, accessToken, userKey, privateAccess) { + if (!pool) { + return; + } + + const insertQuery = ` + INSERT INTO users (user_id, access_token, user_key, private_access) + VALUES ($1, $2, $3, $4) + ON CONFLICT (user_id) + DO UPDATE SET + access_token = EXCLUDED.access_token, + user_key = EXCLUDED.user_key, + private_access = EXCLUDED.private_access + `; + + try { + await pool.query(insertQuery, [ + userId, + accessToken, + userKey, + privateAccess, + ]); + } catch (err) { + if (err.code === "42P01") { + await createAllTables(); + await pool.query(insertQuery, [ + userId, + accessToken, + userKey, + privateAccess, + ]); + } else { + throw err; + } + } +} + +/** + * Stores or updates an Oauth code/userKey pair in the database. + * @param {string} code OAuth code + * @param {string} userKey userKey to associate + */ +/* +export async function storeCodeKey(code, userKey) { + if (!pool) { + return; + } + + const insertQuery = ` + INSERT INTO code_key_map (code, user_key) + VALUES ($1, $2) + ON CONFLICT (code) + DO UPDATE SET user_key = EXCLUDED.user_key + `; + try { + await pool.query(insertQuery, [code, userKey]); + } catch (err) { + if (err.code === "42P01") { + await createAllTables(); + await pool.query(insertQuery, [code, userKey]); + } else { + throw err; + } + } +} +*/ + +/** + * Retrieves the userKey for a given Oauth code from the database. + * @param {string} code OAuth code + * @returns {Promise} userKey or null if not found + */ +/* +export async function getCodeKey(code) { + if (!pool) { + return null; + } + + const query = ` + SELECT user_key FROM code_key_map WHERE code = $1 + `; + let rows; + try { + ({ rows } = await pool.query(query, [code])); + } catch (err) { + if (err.code === "42P01") { + await createAllTables(); + ({ rows } = await pool.query(query, [code])); + } else { + throw err; + } + } + return rows.length > 0 ? rows[0].user_key : null; +} +*/ diff --git a/src/users.js b/src/users.js new file mode 100644 index 00000000..e076f41a --- /dev/null +++ b/src/users.js @@ -0,0 +1,104 @@ +import axios from "axios"; +import { storeUser } from "./common/database.js"; + +/** + * Set user key for a given code + * @param {string} code GitHub authentication code from OAuth process + * @param {string} userKey user key to associate with the user + * @returns {Promise} the user key, unchanged + */ +/* +async function setUserKey(code, userKey) { + await storeCodeKey(code, userKey); + return userKey; +} +*/ + +/** + * Given an access token, return the GitHub login (userId) or null if invalid + * @param {string} accessToken GitHub access token + * @returns {Promise} login name or null if invalid access_token + */ +async function getUnknownUser(accessToken) { + const res = await axios.get("https://api.github.com/user", { + headers: { + Accept: "application/vnd.github.v3+json", + Authorization: `bearer ${accessToken}`, + }, + }); + + return res.data && res.data.login ? res.data.login : null; +} + +/** + * Exchanges OAuth code for access token and returns userId + accessToken + * @param {string} code GitHub authentication code from OAuth process + * @returns {Promise<{userId: string, accessToken: string}>} user_id and access_token of authenticated user + */ +async function githubAuthenticate(code) { + if ( + !process.env.OAUTH_CLIENT_ID || + !process.env.OAUTH_CLIENT_SECRET || + !process.env.OAUTH_REDIRECT_URI + ) { + console.error( + "OAuth Error: One or more required environment variables (OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_REDIRECT_URI) are not set.", + ); + } + + const start = Date.now(); + const params = new URLSearchParams({ + client_id: process.env.OAUTH_CLIENT_ID, + client_secret: process.env.OAUTH_CLIENT_SECRET, + code, + redirect_uri: process.env.OAUTH_REDIRECT_URI, + }); + + try { + const res = await axios.post( + "https://github.com/login/oauth/access_token", + params.toString(), + { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + // TODO: above headers are not in github-trends, so double check + }, + ); + + const body = res.data; + const accessToken = body && body.access_token ? body.access_token : null; + // TODO: verify above parsing logic, as it's new, by ChatGPT + + if (!accessToken) { + throw new Error("OAuth Error: access_token missing from response"); + } + + const userId = await getUnknownUser(accessToken); + + if (!userId) { + throw new Error("OAuth Error: Invalid user_id/access_token"); + } + + console.log("OAuth SignUp", `${Date.now() - start} ms`); + return { userId, accessToken }; + } catch (err) { + if (err.response) { + throw new Error(`OAuth Error: ${err.response.status}`); + } + throw err; + } +} +/** + * Authenticate using the OAuth code and update DB with associated user info. + * + * @param {string} code GitHub authentication code from OAuth process + * @param {boolean} privateAccess whether private access was requested + * @param {string} userKey user key to associate with the user + */ +export async function authenticate(code, privateAccess, userKey) { + const { userId, accessToken } = await githubAuthenticate(code); + // const userKey = await getCodeKey(code); + await storeUser(userId, accessToken, userKey, privateAccess); +}