add login functionality

This commit is contained in:
martin-mfg
2025-09-02 11:07:35 +02:00
parent 5440104585
commit e2a14fc4d1
5 changed files with 281 additions and 11 deletions
@@ -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");
+1
View File
@@ -0,0 +1 @@
../api.func
+18
View File
@@ -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");
};
+154 -11
View File
@@ -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<string|null>} 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;
}
*/
+104
View File
@@ -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<string>} 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<string|null>} 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);
}