Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592// Authorization, 23.04.30.17.47
// Authenticates a user to make sure they are logged in// and their local keys match the user database.// 🧠 (And so they can run authorized server functions.)
import { connect } from "./database.mjs";import * as KeyValue from "./kv.mjs";import { shell } from "./shell.mjs";const dev = process.env.CONTEXT === "dev";
const aestheticBaseURI = "https://aesthetic.us.auth0.com";const sotceBaseURI = "https://sotce.us.auth0.com";
export async function authorize({ authorization }, tenant = "aesthetic") { try { const { got } = await import("got"); const baseURI = tenant === "aesthetic" ? aestheticBaseURI : sotceBaseURI; shell.log(`🔐 Attempting to authorize \`${tenant}\` user...`); const result = ( await got(`${baseURI}/userinfo`, { headers: { Authorization: authorization }, responseType: "json", }) ).body; shell.log(`✅ Authorization successful for \`${tenant}\` user: ${result?.sub}`); return result; } catch (err) { shell.error("❌ Authorization failed:", err?.message || err, err?.code); return undefined; }}
export async function hasAdmin(user, tenant = "aesthetic") { if (tenant === "aesthetic") { const handle = await handleFor(user.sub); return ( user && user.email_verified && handle === "jeffrey" && user.sub === process.env.ADMIN_SUB ); } else if (tenant === "sotce") { const subs = process.env.SOTCE_ADMIN_SUBS?.split(","); if (!subs || subs.length === 0) { // Fallback: check if user email is in admin list const adminEmails = ["me@jas.life", "sotce.net@gmail.com"]; return user && user.email && adminEmails.includes(user.email.toLowerCase()); } const handle = await handleFor(user.sub, "sotce"); return ( user && user.email_verified && ((user.sub === subs[0] && handle === "jeffrey") || (user.sub === subs[1] && (handle === "amelia" || handle === "sotce"))) ); }}
// Get the user ID via their email, allowing a valid user ID as input also.export async function userIDFromEmail(email, tenant = "aesthetic", got, token) { if (email.indexOf("|") !== -1 && email.indexOf("@") === -1) { return email; // Simply return a user sub (the input) if no email is detected. }
try { if (!got) got = (await import("got")).got; if (!token) token = await getAccessToken(got, tenant); const baseURI = tenant === "aesthetic" ? aestheticBaseURI : sotceBaseURI;
const userResponse = await got(`${baseURI}/api/v2/users-by-email`, { searchParams: { email }, headers: { Authorization: `Bearer ${token}` }, responseType: "json", });
const user = userResponse.body[0]; const userID = user?.user_id; return { userID, email_verified: user?.email_verified, tenant }; } catch (error) { shell.error(`Error retrieving user ID from Auth0: ${error}`); return undefined; }}
// Pick between the below functions based on sub prefix.export async function findSisterSub(sub, options) { if (sub.startsWith("sotce-")) { return await aestheticSubFromSotceSub(sub); } else { return await sotceSubFromAestheticSub(sub, options); }}
// Get `aesthetic` user id from a sotce user, if it exists.// TODO: Cache this in redis to be faster? 24.09.01.00.40export async function aestheticSubFromSotceSub(sotceSub) { const emailRes = await userEmailFromID(sotceSub, "sotce"); if (emailRes?.email && emailRes?.email_verified) { const idRes = await userIDFromEmail(emailRes.email, "aesthetic"); if (idRes?.userID && idRes?.email_verified) { return idRes.userID; } } return undefined;}
// Get `sotce` user id from an aesthetic user, if it exists.// TODO: Cache this in redis to be faster? 24.09.01.00.40export async function sotceSubFromAestheticSub(aestheticSub, options) { const emailRes = await userEmailFromID(aestheticSub, "aesthetic"); if (emailRes?.email && emailRes?.email_verified) { const idRes = await userIDFromEmail(emailRes.email, "sotce"); if (idRes?.userID && idRes?.email_verified) { return (options?.prefixed ? "sotce-" : "") + idRes.userID; } } return undefined;}
// Get the user email via their user ID.export async function userEmailFromID(sub, tenant = "aesthetic", got, token) { try { if (!got) got = (await import("got")).got; if (sub.startsWith("sotce-")) tenant = "sotce"; // Switch tenant based on prefix. if (tenant === "sotce") sub = sub.replace("sotce-", ""); const baseURI = tenant === "aesthetic" ? aestheticBaseURI : sotceBaseURI; if (!token) token = await getAccessToken(got, tenant);
const userResponse = await got(`${baseURI}/api/v2/users/${sub}`, { headers: { Authorization: `Bearer ${token}` }, responseType: "json", });
const user = userResponse.body; const email = user?.email; return { email, email_verified: user?.email_verified }; } catch (error) { shell.error(`Error retrieving user email from Auth0 (${tenant}): ${error}`); return undefined; }}
// Takes in a user ID (sub) and returns the user's @handle (preferred) or email.export async function getHandleOrEmail(sub) { try { // Attempt to get the user's handle. const handle = await handleFor(sub); if (handle) return "@" + handle;
// If no handle is found, fetch the user's email from Auth0. const { got } = await import("got"); const token = await getAccessToken(got); // Get access token for auth0. const userResponse = await got( `https://aesthetic.us.auth0.com/api/v2/users/${encodeURIComponent(sub)}`, { headers: { Authorization: `Bearer ${token}` }, responseType: "json" }, );
return userResponse.body.email; } catch (error) { shell.error(`Error retrieving user handle or email: ${error}`); return undefined; }}
// Connects to the Redis cache or MongoDB database to obtain a user's handle// from their ID (across tenants).export async function handleFor(id, tenant = "aesthetic") { // const time = performance.now();
if (id === "all") { // 📖 Get an aggregate list of all handles. const database = await connect(); const collection = database.db.collection("@handles"); const randomHandles = await collection .aggregate([{ $sample: { size: 100 } }, { $project: { handle: 1 } }]) .toArray(); await database.disconnect(); return randomHandles.map((doc) => "@" + doc.handle); } else { // 🙆 Get a specific user handle. if (tenant === "sotce" && !id.startsWith("sotce-")) id = "sotce-" + id; shell.log("Retrieving handle for...", id);
await KeyValue.connect(); const cachedHandle = await KeyValue.get("userIDs", id);
if (cachedHandle) { // await KeyValue.disconnect(); return cachedHandle; }
const database = await connect(); const collection = database.db.collection("@handles"); let existingUser = await collection.findOne({ _id: id });
// If no handle was found then try again on the sister tenant. if (!existingUser) { const sisterSub = await findSisterSub(id, { prefixed: true });
if (sisterSub) { let foundHandle = await KeyValue.get("userIDs", sisterSub); if (foundHandle) { // Make sure to cache the original id for this handle. await KeyValue.set("userIDs", id, foundHandle); await KeyValue.disconnect(); await database.disconnect(); return foundHandle; } else { // Then in the database. existingUser = await collection.findOne({ _id: sisterSub }); id = sisterSub; } } }
// Cache the handle in redis for quick look up. if (existingUser?.handle) { await KeyValue.set("userIDs", existingUser._id, existingUser.handle); }
await database.disconnect(); await KeyValue.disconnect();
// console.log("Time taken...", performance.now() - time); return existingUser?.handle; }}
// Connects to the MongoDB database to obtain a user ID from a handle.// Handle should not be prefixed with "@".
// ❤️🔥// TODO: This could return a "sotce-" prefixed id which// would be incompatible with aesthetic computer if// an account does not exist / this function may need a// `tenant` parameter.export async function userIDFromHandle( handle, database, keepKV, tenant = "aesthetic",) { // Read from redis, otherwise check the database, and store in redis after. let userID; // const time = performance.now(); await KeyValue.connect(); const cachedUserID = await KeyValue.get("@handles", handle);
if (tenant === "aesthetic" && cachedUserID?.startsWith("sotce-")) { return await aestheticSubFromSotceSub(cachedUserID); }
if (!cachedUserID) { // Look in database. // if (dev) console.log("Handle: Looking in database..."); const keepOpen = database; // Keep the db connection if database is defined. // if (dev) console.log("Handle: Connecting...", time); if (!database) database = await connect(); const collection = database.db.collection("@handles"); const user = await collection.findOne({ handle }); userID = user?._id; if (!keepOpen) database.disconnect(); if (tenant === "aesthetic" && userID?.startsWith("sotce-")) { return await aestheticSubFromSotceSub(userID); } } else { // if (dev) console.log("Handle: Found in redis..."); userID = cachedUserID; }
// Cache userID in redis... if (!cachedUserID && userID) { if (dev) shell.log("Caching primary handle key in redis...", handle); await KeyValue.set("@handles", handle, userID); if (!keepKV) await KeyValue.disconnect(); }
// console.log("Time taken...", performance.now() - time); return userID;}
// Connects to MongoDB to find a user's handle from their permahandle (code).// Permahandle format: ac25xxxxx (9 characters)// Returns: { handle, sub } or undefined if not foundexport async function handleFromPermahandle(code, database) { if (!code || typeof code !== "string") return undefined; // Permahandles are exactly 9 chars and start with "ac" if (code.length !== 9 || !code.startsWith("ac")) return undefined; // Check redis cache first await KeyValue.connect(); const cachedHandle = await KeyValue.get("permahandles", code); if (cachedHandle) { await KeyValue.disconnect(); return JSON.parse(cachedHandle); } // Look in database const keepOpen = database; if (!database) database = await connect(); const usersCollection = database.db.collection("users"); const user = await usersCollection.findOne({ code }); if (!user) { if (!keepOpen) await database.disconnect(); await KeyValue.disconnect(); return undefined; } // Get the handle from @handles collection using the user's _id const handlesCollection = database.db.collection("@handles"); const handleDoc = await handlesCollection.findOne({ _id: user._id }); if (!keepOpen) await database.disconnect(); const result = handleDoc ? { handle: handleDoc.handle, sub: user._id } : undefined; // Cache the result in redis if (result) { await KeyValue.set("permahandles", code, JSON.stringify(result)); } await KeyValue.disconnect(); return result;}
// Assume prefixed handle.// ⚠️ TODO: Make sure we are knowing what id we want from what network... 24.08.31.01.21export async function userIDFromHandleOrEmail(handleOrEmail, database, tenant) { if (!handleOrEmail) return; if (handleOrEmail.startsWith("@") || handleOrEmail.indexOf("|") === -1) { const sub = await userIDFromHandle( handleOrEmail.startsWith("@") ? handleOrEmail.slice(1) : handleOrEmail, database, undefined, tenant, ); return sub; } else { return await userIDFromEmail(handleOrEmail, tenant); // Assume email. }}
// Sets the user's email and triggers a re-verification email.export async function setEmailAndReverify( id, email, name, tenant = "aesthetic",) { try { const { got } = await import("got"); const baseURI = tenant === "aesthetic" ? aestheticBaseURI : sotceBaseURI;
const token = await getAccessToken(got, tenant);
shell.log( "👮 📧 Setting and re-verifying email for:", email, "on:", tenant, "via:", id, );
// 1. Update the user's email and ('name' which is equivalent to email // in auth0 but generally unused by Aesthetic Computer.) let updateEmailResponse; try { updateEmailResponse = await got( `${baseURI}/api/v2/users/${encodeURIComponent(id)}`, { method: "PATCH", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, json: { name, email, email_verified: false }, responseType: "json", }, ); } catch (err) { shell.error("🔴 Error:", err); }
if (!updateEmailResponse.body) { throw new Error("Failed to update user email"); }
// 2. Trigger the verification email const verificationResponse = await got( `${baseURI}/api/v2/jobs/verification-email`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, json: { user_id: id }, responseType: "json", }, );
if (!verificationResponse.body) { throw new Error("Failed to send verification email"); }
return { success: true, message: "Email updated and verification email sent successfully!", }; } catch (error) { shell.error(`Error setting email and sending verification: ${error}`); return { success: false, message: error.message, }; }}
// Deletes a user from auth0.export async function deleteUser(userId, tenant = "aesthetic") { try { const { got } = await import("got"); const token = await getAccessToken(got, tenant); const baseURI = tenant === "aesthetic" ? aestheticBaseURI : sotceBaseURI;
await got(`${baseURI}/api/v2/users/${encodeURIComponent(userId)}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}` }, });
shell.log( `❌ User with ID ${userId} deleted from Auth0. Tenant: ${tenant}`, ); return { success: true, message: "User deleted successfully from Auth0." }; } catch (error) { shell.error(`⚠️ Error deleting user from Auth0: ${error}`); return { success: false, message: error.message }; }}
// Queries the total number of signed-up users by including totals in the response.export async function querySignups(tenant = "aesthetic") { try { const { got } = await import("got"); const baseURI = tenant === "aesthetic" ? aestheticBaseURI : sotceBaseURI; const token = await getAccessToken(got, tenant);
const response = await got(`${baseURI}/api/v2/users`, { searchParams: { page: 0, per_page: 1, // Fetch minimal data to reduce overhead. include_totals: true, // Include the total user count. }, headers: { Authorization: `Bearer ${token}` }, responseType: "json", });
return response.body.total || 0; // Return the total user count. } catch (error) { shell.error(`Error querying signups from Auth0: ${error}`); return undefined; }}
// Retrieves daily stats for logins and signups from the Auth0 stats endpoint.//export async function activeUsers(tenant = "aesthetic") { try { const { got } = await import("got"); const baseURI = tenant === "aesthetic" ? aestheticBaseURI : sotceBaseURI; const token = await getAccessToken(got, tenant);
const response = await got(`${baseURI}/api/v2/stats/active-users`, { headers: { Authorization: `Bearer ${token}` }, responseType: "json", });
return response.body; // Returns an array of daily stats. } catch (error) { shell.error(`Error fetching daily stats from Auth0: ${error}`); return undefined; }}
// Retrieves the total count of verified users in the specified tenant.// export async function verifiedUsers(tenant = "aesthetic") {// try {// const { got } = await import("got");// const baseURI = tenant === "aesthetic" ? aestheticBaseURI : sotceBaseURI;// const token = await getAccessToken(got, tenant);//// let page = 0;// let perPage = 100;// let verifiedCount = 0;//// while (true) {// const response = await got(`${baseURI}/api/v2/users`, {// searchParams: {// q: "email_verified:true",// search_engine: "v3",// page,// per_page: perPage,// },// headers: { Authorization: `Bearer ${token}` },// responseType: "json",// });//// const users = response.body;// verifiedCount += users.length;////// if (users.length < perPage) break; // No more pages to fetch.// page++;// console.log("Verified users so far:", verifiedCount, "Page:", page);// }//// shell.log(`✅ Verified users count for tenant ${tenant}: ${verifiedCount}`);// return verifiedCount;// } catch (error) {// shell.error(// `Error retrieving verified users count from ${tenant}: ${error}`,// );// return undefined;// }// }
// Helper function to introduce a delay (in milliseconds).const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// � Token cache for M2M tokens (added 2026.01.28 for boot speed)// Auth0 M2M tokens are valid for 24 hours, we cache for 23 hours to be safeconst tokenCache = { aesthetic: { token: null, expiry: 0 }, sotce: { token: null, expiry: 0 }};const TOKEN_CACHE_MS = 23 * 60 * 60 * 1000; // 23 hours
// 📚 Library (Useful functions used throughout the file.)// Obtain an auth0 access token for our M2M API.async function getAccessToken(got, tenant = "aesthetic") { // 🚀 Check in-memory token cache first const cached = tokenCache[tenant]; if (cached && cached.token && Date.now() < cached.expiry) { shell.log(`🚀 Using cached M2M token for ${tenant} (expires in ${Math.round((cached.expiry - Date.now()) / 1000 / 60)} min)`); return cached.token; }
let baseURI, client_id, client_secret; if (tenant === "aesthetic") { baseURI = aestheticBaseURI; client_id = process.env.AUTH0_M2M_CLIENT_ID; client_secret = process.env.AUTH0_M2M_SECRET; } else { // assume tenant is `sotce`. baseURI = sotceBaseURI; client_id = process.env.SOTCE_AUTH0_M2M_CLIENT_ID; client_secret = process.env.SOTCE_AUTH0_M2M_SECRET; }
shell.log(`🔑 Getting fresh access token for ${tenant}`); shell.log(` Client ID: ${client_id?.substring(0, 10)}...`); shell.log(` Secret length: ${client_secret?.length || 0}`);
const tokenResponse = await got(`${baseURI}/oauth/token`, { method: "POST", headers: { "Content-Type": "application/json" }, json: { client_id, client_secret, audience: `${baseURI}/api/v2/`, grant_type: "client_credentials", // Use "client_credentials" for M2M }, responseType: "json", });
const token = tokenResponse.body.access_token; // 🚀 Cache the token tokenCache[tenant] = { token, expiry: Date.now() + TOKEN_CACHE_MS }; shell.log(`💾 Cached M2M token for ${tenant}`);
return token;}