This PR mainly refactors the code by extracting a "core" package which is then used by the "backend" and "frontend" apps. Apart from this the PR also contains several smaller changes like fixing dependabot, upgrading dependencies, aligned "package.json" files, added tests, ... addresses step 1 of #32 closes #136 --------- Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
262 lines
6.2 KiB
JavaScript
262 lines
6.2 KiB
JavaScript
export let pool = null;
|
|
if (process.env.POSTGRES_URL) {
|
|
const { Pool } = await import("pg");
|
|
pool = new Pool({
|
|
connectionString: process.env.POSTGRES_URL,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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 authenticated_users (
|
|
user_id TEXT PRIMARY KEY,
|
|
access_token TEXT NOT NULL,
|
|
user_key TEXT,
|
|
private_access BOOLEAN NOT NULL DEFAULT false
|
|
);
|
|
`);
|
|
}
|
|
|
|
/**
|
|
* Stores or updates a request in the database.
|
|
*/
|
|
export async function storeRequest(req) {
|
|
if (!pool) {
|
|
return;
|
|
}
|
|
|
|
const isBypass = req.headers && req.headers["x-bypass-store"];
|
|
const insertQuery = isBypass
|
|
? `
|
|
INSERT INTO requests (request, requested_at)
|
|
VALUES ($1, NOW())
|
|
ON CONFLICT (request)
|
|
DO UPDATE SET requested_at = EXCLUDED.requested_at
|
|
`
|
|
: `
|
|
INSERT INTO requests (request, requested_at, user_requested_at)
|
|
VALUES ($1, NOW(), NOW())
|
|
ON CONFLICT (request)
|
|
DO UPDATE SET requested_at = EXCLUDED.requested_at, user_requested_at = EXCLUDED.user_requested_at
|
|
`;
|
|
|
|
try {
|
|
await pool.query(insertQuery, [req.url]);
|
|
} catch (err) {
|
|
// Check for undefined_table error (SQLSTATE 42P01)
|
|
if (err.code === "42P01") {
|
|
await createAllTables();
|
|
// Retry the insert after creating the table
|
|
await pool.query(insertQuery, [req.url]);
|
|
} else {
|
|
throw err; // Re-throw if it's some other error
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes all requests older than the specified time from the database.
|
|
*/
|
|
export async function deleteOldRequests(interval) {
|
|
if (!pool) {
|
|
return;
|
|
}
|
|
|
|
const deleteQuery = `
|
|
DELETE FROM requests
|
|
WHERE user_requested_at < NOW() - INTERVAL '${interval}'
|
|
`;
|
|
try {
|
|
let result = await pool.query(deleteQuery);
|
|
console.log(`Deleted ${result.rowCount} old requests.`);
|
|
} catch (err) {
|
|
if (err.code === "42P01") {
|
|
console.log("Error deleting requests, table doesn't exist");
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches all requests which are between minInterval and maxInterval old.
|
|
*
|
|
* @returns {Promise<string[]>} Array of all requests between minInterval and maxInterval old.
|
|
*/
|
|
export async function getRecentRequests(minInterval, maxInterval) {
|
|
if (!pool) {
|
|
return [];
|
|
}
|
|
|
|
const query = `
|
|
SELECT request
|
|
FROM requests
|
|
WHERE requested_at >= NOW() - INTERVAL '${maxInterval}'
|
|
AND requested_at < NOW() - INTERVAL '${minInterval}'
|
|
ORDER BY requested_at ASC
|
|
`;
|
|
let rows;
|
|
try {
|
|
({ rows } = await pool.query(query));
|
|
} catch (err) {
|
|
if (err.code === "42P01") {
|
|
console.log("Error fetching requests, table doesn't exist");
|
|
} 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 authenticated_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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a user from the database.
|
|
*
|
|
* @param userKey user key of the user which is to be deleted.
|
|
*/
|
|
export async function deleteUser(userKey) {
|
|
if (!pool) {
|
|
return;
|
|
}
|
|
|
|
const deleteQuery = `
|
|
DELETE FROM authenticated_users
|
|
WHERE user_key = $1
|
|
`;
|
|
try {
|
|
await pool.query(deleteQuery, [userKey]);
|
|
} catch (err) {
|
|
if (err.code === "42P01") {
|
|
console.log("Error deleting user, table doesn't exist");
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches token and private access status for a given user_key.
|
|
*
|
|
* @param {string} userKey user key of the user to fetch information for
|
|
* @returns {Promise<{token: string, privateAccess: boolean} | null>} token and private access status, or null if user not found
|
|
*/
|
|
export async function getUserAccessByKey(userKey) {
|
|
if (!pool) {
|
|
return null;
|
|
}
|
|
|
|
const query = `
|
|
SELECT access_token, private_access
|
|
FROM authenticated_users
|
|
WHERE user_key = $1
|
|
LIMIT 1
|
|
`;
|
|
try {
|
|
const { rows } = await pool.query(query, [userKey]);
|
|
if (rows.length === 0) {
|
|
return null;
|
|
}
|
|
return {
|
|
token: rows[0].access_token,
|
|
privateAccess: rows[0].private_access,
|
|
};
|
|
} catch (err) {
|
|
if (err.code === "42P01") {
|
|
return null;
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches token and private access status for a given username.
|
|
*
|
|
* @param {string} userName GitHub username of the user to fetch information for
|
|
* @returns {Promise<{token: string, privateAccess: boolean} | null>} token and private access status, or null if user not found
|
|
*/
|
|
export async function getUserAccessByName(userName) {
|
|
if (!pool) {
|
|
return null;
|
|
}
|
|
|
|
const query = `
|
|
SELECT access_token, private_access
|
|
FROM authenticated_users
|
|
WHERE user_id = $1
|
|
LIMIT 1
|
|
`;
|
|
try {
|
|
const { rows } = await pool.query(query, [userName]);
|
|
if (rows.length === 0) {
|
|
return null;
|
|
}
|
|
return {
|
|
token: rows[0].access_token,
|
|
privateAccess: rows[0].private_access,
|
|
};
|
|
} catch (err) {
|
|
if (err.code === "42P01") {
|
|
return null;
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|