add login functionality
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
../api.func
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user