import "dotenv/config"; import "dotenv/config"; import express from "express"; import cors from "cors"; import { adminSupabase } from "./supabase"; import { emailService } from "./email"; import { randomUUID, createHash, createVerify, randomBytes, createHmac } from "crypto"; import * as https from "https"; import * as http from "http"; // httpsPost / httpsGet — use Node's https module which respects /etc/hosts (unlike fetch/undici) function httpsRequest(url: string, options: { method?: string; headers?: Record; body?: string }): Promise<{ status: number; text: () => string; json: () => any }> { return new Promise((resolve, reject) => { const parsed = new URL(url); const lib = parsed.protocol === "https:" ? https : http; const reqOpts = { hostname: parsed.hostname, port: parsed.port || (parsed.protocol === "https:" ? 443 : 80), path: parsed.pathname + parsed.search, method: options.method || "GET", headers: options.headers || {}, }; const req = lib.request(reqOpts, (res) => { const chunks: Buffer[] = []; res.on("data", (c) => chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c))); res.on("end", () => { const raw = Buffer.concat(chunks).toString("utf8"); resolve({ status: res.statusCode || 0, text: () => raw, json: () => JSON.parse(raw), }); }); }); req.on("error", reject); if (options.body) req.write(options.body); req.end(); }); } import blogIndexHandler from "../api/blog/index"; import blogSlugHandler from "../api/blog/[slug]"; import aiChatHandler from "../api/ai/chat"; import aiTitleHandler from "../api/ai/title"; // Developer API Keys handlers import { listKeys, createKey, deleteKey, updateKey, getKeyStats, getProfile, updateProfile, } from "../api/developer/keys"; // Discord Interactions Handler const handleDiscordInteractions = async ( req: express.Request, res: express.Response, ) => { try { const signature = req.get("x-signature-ed25519"); const timestamp = req.get("x-signature-timestamp"); const rawBody = req.body instanceof Buffer ? req.body : Buffer.from(JSON.stringify(req.body), "utf8"); const bodyString = rawBody.toString("utf8"); const publicKey = process.env.DISCORD_PUBLIC_KEY; console.log("[Discord] Interaction received at", new Date().toISOString()); if (!publicKey) { console.error("[Discord] DISCORD_PUBLIC_KEY not set"); return res.status(401).json({ error: "Server not configured" }); } if (!signature || !timestamp) { console.error( "[Discord] Missing headers - signature:", !!signature, "timestamp:", !!timestamp, ); return res.status(401).json({ error: "Invalid request" }); } // Verify signature const message = `${timestamp}${bodyString}`; const signatureBuffer = Buffer.from(signature, "hex"); const verifier = createVerify("ed25519"); verifier.update(message); const isValid = verifier.verify(publicKey, signatureBuffer); if (!isValid) { console.error("[Discord] Signature verification failed"); return res.status(401).json({ error: "Invalid signature" }); } const interaction = JSON.parse(bodyString); console.log("[Discord] Valid interaction type:", interaction.type); // Discord sends a PING to verify the endpoint if (interaction.type === 1) { console.log("[Discord] āœ“ PING verified"); return res.json({ type: 1 }); } // Handle APPLICATION_COMMAND (slash commands) if (interaction.type === 2) { const commandName = interaction.data.name; console.log("[Discord] Slash command received:", commandName); // /creators command if (commandName === "creators") { const arm = interaction.data.options?.[0]?.value; const armFilter = arm ? ` (${arm})` : " (all arms)"; return res.json({ type: 4, data: { content: `šŸ” Browse AeThex Creators${armFilter}\n\nšŸ‘‰ [Open Creator Directory](https://aethex.dev/creators${arm ? `?arm=${arm}` : ""})`, flags: 0, }, }); } // /opportunities command if (commandName === "opportunities") { const arm = interaction.data.options?.[0]?.value; const armFilter = arm ? ` (${arm})` : " (all arms)"; return res.json({ type: 4, data: { content: `šŸ’¼ Find Opportunities on Nexus${armFilter}\n\nšŸ‘‰ [Browse Opportunities](https://aethex.dev/opportunities${arm ? `?arm=${arm}` : ""})`, flags: 0, }, }); } // /nexus command if (commandName === "nexus") { return res.json({ type: 4, data: { content: `✨ **AeThex Nexus** - The Talent Marketplace\n\nšŸ”— [Open Nexus](https://aethex.dev/nexus)\n\n**Quick Links:**\n• šŸ” [Browse Creators](https://aethex.dev/creators)\n• šŸ’¼ [Find Opportunities](https://aethex.dev/opportunities)\n• šŸ“Š [View Metrics](https://aethex.dev/admin)`, flags: 0, }, }); } // /verify command - Generate verification code and link if (commandName === "verify") { try { const discordId = interaction.member?.user?.id; if (!discordId) { return res.json({ type: 4, data: { content: "āŒ Could not get your Discord ID", flags: 64, }, }); } // Generate verification code (random 6-digit) const verificationCode = Math.random() .toString(36) .substring(2, 8) .toUpperCase(); const expiresAt = new Date(Date.now() + 15 * 60 * 1000).toISOString(); // 15 min // Store verification code in Supabase const { error } = await adminSupabase .from("discord_verifications") .insert([ { discord_id: discordId, verification_code: verificationCode, expires_at: expiresAt, }, ]); if (error) { console.error("Error storing verification code:", error); return res.json({ type: 4, data: { content: "āŒ Error generating verification code. Please try again.", flags: 64, }, }); } const verifyUrl = `https://aethex.dev/discord-verify?code=${verificationCode}`; return res.json({ type: 4, data: { content: `āœ… **Verification Code: \`${verificationCode}\`**\n\n��� [Click here to verify your account](${verifyUrl})\n\nā±ļø This code expires in 15 minutes.`, flags: 0, }, }); } catch (error) { console.error("Error in /verify command:", error); return res.json({ type: 4, data: { content: "�� An error occurred. Please try again later.", flags: 64, }, }); } } // /set-realm command - Choose primary arm if (commandName === "set-realm") { const realmChoice = interaction.data.options?.[0]?.value; if (!realmChoice) { return res.json({ type: 4, data: { content: "āŒ Please select a realm", flags: 64, }, }); } const realmMap: any = { labs: "šŸ”¬ Labs", gameforge: "šŸŽ® GameForge", corp: "šŸ’¼ Corp", foundation: "šŸ¤ Foundation", devlink: "šŸ”— Dev-Link", }; return res.json({ type: 4, data: { content: `āœ… You've selected **${realmMap[realmChoice] || realmChoice}** as your primary realm!\n\nšŸ“ Your role will be assigned based on your selection.`, flags: 0, }, }); } // /profile command - Show user profile if (commandName === "profile") { const discordId = interaction.member?.user?.id; const username = interaction.member?.user?.username; return res.json({ type: 4, data: { content: `šŸ‘¤ **Your AeThex Profile**\n\n**Discord:** ${username} (\`${discordId}\`)\n\nšŸ”— [View Full Profile](https://aethex.dev/profile)\n\n**Quick Actions:**\n• \`/set-realm\` - Choose your primary arm\n• \`/verify\` - Link your account\n• \`/verify-role\` - Check your assigned roles`, flags: 0, }, }); } // /unlink command - Disconnect Discord if (commandName === "unlink") { const discordId = interaction.member?.user?.id; return res.json({ type: 4, data: { content: `��� **Account Unlinked**\n\nYour Discord account (\`${discordId}\`) has been disconnected from AeThex.\n\nTo link again, use \`/verify\``, flags: 0, }, }); } // /verify-role command - Check assigned roles if (commandName === "verify-role") { return res.json({ type: 4, data: { content: `āœ… **Discord Roles**\n\nYour assigned AeThex roles are shown below.\n\n���� [View Full Profile](https://aethex.dev/profile)`, flags: 0, }, }); } // Default command response return res.json({ type: 4, data: { content: `✨ AeThex - Advanced Development Platform\n\n**Available Commands:**\n• \`/creators [arm]\` - Browse creators across AeThex arms\n• \`/opportunities [arm]\` - Find job opportunities and collaborations\n• \`/nexus\` - Explore the Talent Marketplace\n• \`/verify\` - Link your Discord account\n• \`/set-realm\` - Choose your primary realm\n• \`/profile\` - View your AeThex profile\n• \`/unlink\` - Disconnect your account\n• \`/verify-role\` - Check your assigned Discord roles`, flags: 0, }, }); } // For MESSAGE_COMPONENT interactions (buttons, etc.) if (interaction.type === 3) { console.log( "[Discord] Message component interaction:", interaction.data.custom_id, ); return res.json({ type: 4, data: { content: "Button clicked - feature coming soon!" }, }); } // Acknowledge all other interactions return res.json({ type: 4, data: { content: "Interaction acknowledged" }, }); } catch (err: any) { console.error("[Discord] Error:", err?.message); return res.status(500).json({ error: "Server error" }); } }; export function createServer() { const app = express(); // Discord endpoint MUST be defined BEFORE any body parsing middleware // and needs raw body for signature verification app.post( "/api/discord/interactions", express.raw({ type: "application/json" }), handleDiscordInteractions, ); // Middleware app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Allow Discord to embed the activity in iframes app.use((req, res, next) => { // Allow embedding in iframes (Discord Activities need this) res.setHeader("X-Frame-Options", "ALLOWALL"); // Allow Discord to access the iframe res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader( "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS", ); res.setHeader( "Access-Control-Allow-Headers", "Content-Type, Authorization, x-signature-ed25519, x-signature-timestamp", ); next(); }); // Subdomain detection middleware for aethex.me and aethex.space app.use((req, res, next) => { const host = (req.headers.host || "").toLowerCase(); const forwarded = ( (req.headers["x-forwarded-host"] as string) || "" ).toLowerCase(); const hostname = forwarded || host; // Parse subdomain let subdomain = ""; let domain = ""; if (hostname.includes("aethex.me")) { const parts = hostname.split("."); if (parts.length > 2) { subdomain = parts[0]; domain = "aethex.me"; } } else if (hostname.includes("aethex.space")) { const parts = hostname.split("."); if (parts.length > 2) { subdomain = parts[0]; domain = "aethex.space"; } } // Attach subdomain info to request (req as any).subdomainInfo = { subdomain, domain, isCreatorPassport: domain === "aethex.me", isProjectPassport: domain === "aethex.space", }; if (subdomain) { console.log("[Subdomain] Detected:", { hostname, subdomain, domain, isCreatorPassport: domain === "aethex.me", isProjectPassport: domain === "aethex.space", }); } next(); }); // Subdomain Passport Data API - returns JSON for the React component to use app.get("/api/passport/subdomain-data/:username", async (req, res) => { try { const username = String(req.params.username || "") .toLowerCase() .trim(); if (!username) { return res.status(400).json({ error: "username required" }); } console.log("[Passport Data API] Fetching creator:", username); const { data: user, error } = await adminSupabase .from("user_profiles") .select( "id, username, full_name, bio, avatar_url, location, website_url, github_url, linkedin_url, twitter_url, roles, level, total_xp, user_type, experience_level, current_streak, longest_streak, created_at, updated_at", ) .eq("username", username) .single(); if (error || !user) { console.log("[Passport Data API] Creator not found:", username); return res.status(404).json({ error: "Creator not found", username }); } // Fetch achievements const { data: achievements = [] } = await adminSupabase .from("user_achievements") .select( ` achievement_id, achievements( id, title, description, icon, category ) `, ) .eq("user_id", user.id); return res.json({ type: "creator", user, achievements: achievements .map((a: any) => a.achievements) .filter(Boolean), domain: "aethex.me", }); } catch (e: any) { console.error("[Passport Data API] Error:", e?.message); return res.status(500).json({ error: e?.message || "Failed to fetch creator passport", }); } }); app.get("/api/passport/project-data/:projectSlug", async (req, res) => { try { const projectSlug = String(req.params.projectSlug || "") .toLowerCase() .trim(); if (!projectSlug) { return res.status(400).json({ error: "projectSlug required" }); } console.log("[Passport Data API] Fetching project:", projectSlug); const { data: project, error } = await adminSupabase .from("projects") .select( "id, slug, name, description, logo_url, banner_url, website_url, github_url, team_size, created_at, updated_at", ) .eq("slug", projectSlug) .single(); if (error || !project) { console.log("[Passport Data API] Project not found:", projectSlug); return res .status(404) .json({ error: "Project not found", projectSlug }); } return res.json({ type: "group", group: project, domain: "aethex.space", }); } catch (e: any) { console.error("[Passport Data API] Error:", e?.message); return res.status(500).json({ error: e?.message || "Failed to fetch project passport", }); } }); // Health check endpoint - proxies to Discord bot health app.get("/health", async (_req, res) => { try { const botHealthPort = process.env.HEALTH_PORT || 8080; const response = await fetch(`http://localhost:${botHealthPort}/health`); const data = await response.json(); res.json({ status: "online", platform: "aethex.dev", bot: data, timestamp: new Date().toISOString(), }); } catch (error: any) { res.json({ status: "online", platform: "aethex.dev", bot: { status: "offline", error: error?.message || "Bot unreachable" }, timestamp: new Date().toISOString(), }); } }); // Maintenance Mode API endpoints const ADMIN_ROLES = ["admin", "super_admin", "staff", "owner"]; // In-memory maintenance state (fallback when DB table doesn't exist) let maintenanceModeCache: boolean | null = null; // Get maintenance status (public) app.get("/api/admin/platform/maintenance", async (_req, res) => { try { // Try database first const { data, error } = await adminSupabase .from("platform_settings") .select("value") .eq("key", "maintenance_mode") .single(); if (error) { // If table doesn't exist, use env var or in-memory cache if (error.code === "42P01" || error.message?.includes("does not exist")) { const envMaintenance = process.env.MAINTENANCE_MODE === "true"; res.json({ maintenance_mode: maintenanceModeCache ?? envMaintenance }); return; } // Row not found is OK, means maintenance is off if (error.code !== "PGRST116") { throw error; } } const isActive = data?.value === "true" || data?.value === true; maintenanceModeCache = isActive; res.json({ maintenance_mode: isActive }); } catch (e: any) { // Silently fall back to env var or cache (error is expected if table doesn't exist yet) const envMaintenance = process.env.MAINTENANCE_MODE === "true"; res.json({ maintenance_mode: maintenanceModeCache ?? envMaintenance }); } }); // Toggle maintenance mode (admin only) app.post("/api/admin/platform/maintenance", async (req, res) => { try { const authHeader = req.headers.authorization; if (!authHeader?.startsWith("Bearer ")) { return res.status(401).json({ error: "Unauthorized" }); } const token = authHeader.substring(7); const { data: { user }, error: authError } = await adminSupabase.auth.getUser(token); if (authError || !user) { return res.status(401).json({ error: "Invalid token" }); } // Check if user has admin role const { data: roles } = await adminSupabase .from("user_roles") .select("role") .eq("user_id", user.id); const userRoles = roles?.map((r: any) => r.role?.toLowerCase()) || []; const isAdmin = userRoles.some((role: string) => ADMIN_ROLES.includes(role)); if (!isAdmin) { return res.status(403).json({ error: "Admin access required" }); } const rawValue = req.body?.maintenance_mode; const newMaintenanceMode = rawValue === true || rawValue === "true"; // Try to upsert in database const { error } = await adminSupabase .from("platform_settings") .upsert({ key: "maintenance_mode", value: String(newMaintenanceMode), updated_at: new Date().toISOString(), updated_by: user.id, }, { onConflict: "key" }); if (error) { // If table doesn't exist, just use in-memory cache if (error.code === "42P01" || error.message?.includes("does not exist")) { maintenanceModeCache = newMaintenanceMode; console.log(`[Maintenance] Mode set to ${newMaintenanceMode} (in-memory) by ${user.email}`); return res.json({ maintenance_mode: newMaintenanceMode }); } throw error; } maintenanceModeCache = newMaintenanceMode; console.log(`[Maintenance] Mode set to ${newMaintenanceMode} by ${user.email}`); res.json({ maintenance_mode: newMaintenanceMode }); } catch (e: any) { console.error("[Maintenance] Error toggling:", e?.message); res.status(500).json({ error: e?.message || "Failed to toggle maintenance mode" }); } }); // Example API routes app.get("/api/ping", (_req, res) => { const ping = process.env.PING_MESSAGE ?? "ping"; res.json({ message: ping }); }); // API: Creator passport lookup by subdomain (aethex.me) app.get("/api/passport/subdomain/:username", async (req, res) => { try { const username = String(req.params.username || "") .toLowerCase() .trim(); if (!username) { return res.status(400).json({ error: "username required" }); } const { data, error } = await adminSupabase .from("user_profiles") .select( "id, username, full_name, avatar_url, user_type, bio, created_at", ) .eq("username", username) .single(); if (error || !data) { return res.status(404).json({ error: "Creator not found", username, }); } return res.json({ type: "creator", user: data, domain: "aethex.me", }); } catch (e: any) { console.error("[Passport Subdomain] Error:", e?.message); return res.status(500).json({ error: e?.message || "Failed to fetch creator passport", }); } }); // API: Project passport lookup by subdomain (aethex.space) app.get("/api/passport/project/:projectname", async (req, res) => { try { const projectname = String(req.params.projectname || "") .toLowerCase() .trim(); if (!projectname) { return res.status(400).json({ error: "projectname required" }); } // First try exact match by name let { data, error } = await (adminSupabase as any) .from("projects") .select("id, title, slug, description, user_id, created_at, updated_at, status, image_url, website") .eq("slug", projectname) .single(); // If not found by slug, try by title (case-insensitive) if (error && error.code === "PGRST116") { const response = await (adminSupabase as any) .from("projects") .select("id, title, slug, description, user_id, created_at, updated_at, status, image_url, website") .ilike("title", projectname); if (response.data && response.data.length > 0) { data = response.data[0]; error = null; } } if (error || !data) { return res.status(404).json({ error: "Project not found", projectname, }); } // Fetch project owner info const { data: owner } = await adminSupabase .from("user_profiles") .select("id, username, full_name, avatar_url") .eq("id", (data as any).user_id) .maybeSingle(); return res.json({ type: "project", project: data, owner, domain: "aethex.space", }); } catch (e: any) { console.error("[Project Subdomain] Error:", e?.message); return res.status(500).json({ error: e?.message || "Failed to fetch project passport", }); } }); // Public project detail by ID app.get("/api/projects/:projectId", async (req, res) => { try { const { projectId } = req.params; const { data: project, error } = await adminSupabase .from("projects") .select("id, title, description, status, technologies, github_url, live_url, image_url, engine, priority, progress, created_at, updated_at, user_id, owner_user_id") .eq("id", projectId) .single(); if (error || !project) { return res.status(404).json({ error: "Project not found" }); } const ownerId = (project as any).owner_user_id || (project as any).user_id; const { data: owner } = ownerId ? await adminSupabase .from("user_profiles") .select("id, username, full_name, avatar_url") .eq("id", ownerId) .maybeSingle() : { data: null }; return res.json({ project, owner: owner ?? null }); } catch (e: any) { return res.status(500).json({ error: e?.message || "Failed to fetch project" }); } }); // DevConnect REST proxy (GET only) app.get("/api/devconnect/rest/:table", async (req, res) => { try { const base = process.env.DEVCONNECT_URL; const key = process.env.DEVCONNECT_ANON_KEY; if (!base || !key) return res.status(500).json({ error: "DevConnect env not set" }); const table = String(req.params.table || "").replace( /[^a-zA-Z0-9_]/g, "", ); const qs = req.url.includes("?") ? req.url.substring(req.url.indexOf("?")) : ""; const url = `${base}/rest/v1/${table}${qs}`; const r = await fetch(url, { headers: { apikey: key, Authorization: `Bearer ${key}`, Accept: "application/json", }, }); const text = await r.text(); if (!r.ok) return res.status(r.status).send(text); res.setHeader( "content-type", r.headers.get("content-type") || "application/json", ); return res.status(200).send(text); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/auth/send-verification-email", async (req, res) => { const { email, redirectTo, fullName } = (req.body || {}) as { email?: string; redirectTo?: string; fullName?: string | null; }; if (!email) { return res.status(400).json({ error: "email is required" }); } if (!adminSupabase?.auth?.admin) { return res .status(500) .json({ error: "Supabase admin client unavailable" }); } try { const fallbackRedirect = process.env.EMAIL_VERIFY_REDIRECT ?? process.env.PUBLIC_BASE_URL ?? process.env.SITE_URL ?? "https://aethex.biz/login"; const redirectUrl = typeof redirectTo === "string" && redirectTo.startsWith("http") ? redirectTo : fallbackRedirect; const { data, error } = await adminSupabase.auth.admin.generateLink({ type: "signup", email, options: { redirectTo: redirectUrl, }, } as any); if (error) { console.error("[API] generateLink error:", { message: error?.message || String(error), status: (error as any)?.status || null, code: (error as any)?.code || null, details: (error as any)?.details || null, }); const errMsg = typeof error === "string" ? error : error?.message || JSON.stringify(error); return res .status((error as any)?.status ?? 500) .json({ error: errMsg }); } const actionLink = (data as any)?.properties?.action_link ?? (data as any)?.properties?.verification_link ?? null; if (!actionLink) { return res .status(500) .json({ error: "Failed to generate verification link" }); } const displayName = (data as any)?.user?.user_metadata?.full_name ?? fullName ?? null; if (!emailService.isConfigured) { console.warn( "[API] Email service not configured. SMTP env vars missing:", { hasHost: Boolean(process.env.SMTP_HOST), hasUser: Boolean(process.env.SMTP_USER), hasPassword: Boolean(process.env.SMTP_PASSWORD), }, ); return res.json({ sent: false, verificationUrl: actionLink, message: "Email service not configured. Please set SMTP_HOST, SMTP_USER, and SMTP_PASSWORD to enable email sending.", }); } try { await emailService.sendVerificationEmail({ to: email, verificationUrl: actionLink, fullName: displayName, }); console.log("[API] Verification email sent successfully to:", email); return res.json({ sent: true, verificationUrl: actionLink }); } catch (emailError: any) { console.error("[API] sendVerificationEmail threw error:", { message: emailError?.message || String(emailError), code: emailError?.code || null, response: emailError?.response || null, }); // Return with manual link as fallback even if email fails return res.status(200).json({ sent: false, verificationUrl: actionLink, message: `Email delivery failed: ${emailError?.message || "SMTP error"}. Use manual link to verify.`, }); } } catch (error: any) { console.error("[API] send verification email failed", error); return res .status(500) .json({ error: error?.message || "Unexpected error" }); } }); // Org domain magic-link sender (Aethex) app.post("/api/auth/send-org-link", async (req, res) => { try { const { email, redirectTo } = (req.body || {}) as { email?: string; redirectTo?: string; }; const target = String(email || "") .trim() .toLowerCase(); if (!target) return res.status(400).json({ error: "email is required" }); const allowed = /@aethex\.dev$/i.test(target); if (!allowed) return res.status(403).json({ error: "domain not allowed" }); if (!adminSupabase?.auth?.admin) { return res.status(500).json({ error: "Supabase admin unavailable" }); } const fallbackRedirect = process.env.EMAIL_VERIFY_REDIRECT ?? process.env.PUBLIC_BASE_URL ?? process.env.SITE_URL ?? "https://aethex.dev"; const toUrl = typeof redirectTo === "string" && redirectTo.startsWith("http") ? redirectTo : fallbackRedirect; const { data, error } = await adminSupabase.auth.admin.generateLink({ type: "magiclink" as any, email: target, options: { redirectTo: toUrl }, } as any); if (error) { return res.status(500).json({ error: error.message || String(error) }); } const actionLink = (data as any)?.properties?.action_link ?? (data as any)?.properties?.verification_link ?? null; if (!actionLink) { return res.status(500).json({ error: "Failed to generate magic link" }); } if (!emailService.isConfigured) { return res.json({ sent: false, verificationUrl: actionLink }); } await emailService.sendVerificationEmail({ to: target, verificationUrl: actionLink, fullName: null, }); return res.json({ sent: true }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/auth/check-verification", async (req, res) => { const { email } = (req.body || {}) as { email?: string }; if (!email) { return res.status(400).json({ error: "email is required" }); } if (!adminSupabase) { return res .status(500) .json({ error: "Supabase admin client unavailable" }); } try { const targetEmail = String(email).trim().toLowerCase(); // Prefer GoTrue Admin listUsers; fall back to pagination if email filter unsupported const admin = (adminSupabase as any)?.auth?.admin; if (!admin) { return res.status(500).json({ error: "Auth admin unavailable" }); } let user: any = null; let listResp: any = null; try { listResp = await admin.listUsers({ page: 1, perPage: 200, email: targetEmail, } as any); } catch (e) { listResp = null; } const initialUsers: any[] = (listResp?.data?.users as any[]) || []; user = initialUsers.find( (u: any) => String(u?.email || "").toLowerCase() === targetEmail, ) || null; if (!user) { // Pagination fallback (limited scan) for (let page = 1; page <= 5 && !user; page++) { const resp = await admin .listUsers({ page, perPage: 200 } as any) .catch(() => null); const users = (resp?.data?.users as any[]) || []; user = users.find( (u: any) => String(u?.email || "").toLowerCase() === targetEmail, ) || null; } } if (!user) { return res.json({ verified: false, user: null, reason: "not_found" }); } const verified = Boolean(user?.email_confirmed_at || user?.confirmed_at); if (verified) { try { const { data: ach, error: aErr } = await adminSupabase .from("achievements") .select("id") .eq("name", "Founding Member") .maybeSingle(); if (!aErr && ach?.id) { const { error: uaErr } = await adminSupabase .from("user_achievements") .upsert( { user_id: user.id, achievement_id: ach.id }, { onConflict: "user_id,achievement_id" as any }, ); if (uaErr) { console.warn("Failed to award Founding Member:", uaErr); } } } catch (awardErr) { console.warn("Awarding achievement on verification failed", awardErr); } } return res.json({ verified, user }); } catch (e: any) { console.error("[API] check verification exception", e); return res.status(500).json({ error: e?.message || String(e) }); } }); // Storage administration endpoints (service role) app.post("/api/storage/ensure-buckets", async (_req, res) => { if (!adminSupabase) { return res .status(500) .json({ error: "Supabase admin client unavailable" }); } try { const targets = [ { name: "avatars", public: true }, { name: "banners", public: true }, { name: "post_media", public: true }, ]; const { data: buckets } = await ( adminSupabase as any ).storage.listBuckets(); const existing = new Set((buckets || []).map((b: any) => b.name)); const created: string[] = []; for (const t of targets) { if (!existing.has(t.name)) { const { error } = await (adminSupabase as any).storage.createBucket( t.name, { public: t.public }, ); if (error) { console.warn("Failed to create bucket", t.name, error); } else { created.push(t.name); } } } return res.json({ ok: true, created }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // Admin-backed API (service role) try { const ownerEmail = ( process.env.AETHEX_OWNER_EMAIL || "mrpiglr@gmail.com" ).toLowerCase(); const isTableMissing = (err: any) => { const code = err?.code; const message = String(err?.message || err?.hint || err?.details || ""); return ( code === "42P01" || message.includes("relation") || message.includes("does not exist") ); }; // Roblox OAuth: start (build authorize URL with PKCE and redirect) app.get("/api/roblox/oauth/start", (req, res) => { try { const clientId = process.env.ROBLOX_OAUTH_CLIENT_ID; if (!clientId) return res.status(500).json({ error: "Roblox OAuth not configured" }); const baseSite = process.env.PUBLIC_BASE_URL || process.env.SITE_URL || "https://aethex.dev"; const redirectUri = typeof req.query.redirect_uri === "string" && req.query.redirect_uri.startsWith("http") ? String(req.query.redirect_uri) : process.env.ROBLOX_OAUTH_REDIRECT_URI || `${baseSite}/roblox-callback`; const scope = String( req.query.scope || process.env.ROBLOX_OAUTH_SCOPE || "openid", ); const state = String(req.query.state || randomUUID()); // PKCE const codeVerifier = Buffer.from(randomUUID() + randomUUID()) .toString("base64url") .slice(0, 64); const codeChallenge = createHash("sha256") .update(codeVerifier) .digest("base64") .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/g, ""); const params = new URLSearchParams({ client_id: clientId, response_type: "code", redirect_uri: redirectUri, scope, state, code_challenge: codeChallenge, code_challenge_method: "S256", }); const authorizeUrl = `https://apis.roblox.com/oauth/authorize?${params.toString()}`; // set short-lived cookies for verifier/state (for callback validation) const secure = req.secure || req.get("x-forwarded-proto") === "https" || process.env.NODE_ENV === "production"; res.cookie("roblox_oauth_state", state, { httpOnly: true, sameSite: "lax", secure, maxAge: 10 * 60 * 1000, path: "/", }); res.cookie("roblox_oauth_code_verifier", codeVerifier, { httpOnly: true, sameSite: "lax", secure, maxAge: 10 * 60 * 1000, path: "/", }); if (String(req.query.json || "").toLowerCase() === "true") { return res.json({ authorizeUrl, state }); } return res.redirect(302, authorizeUrl); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // Discord OAuth: start authorization flow app.get("/api/discord/oauth/start", (req, res) => { try { const clientId = process.env.DISCORD_CLIENT_ID; if (!clientId) { return res .status(500) .json({ error: "Discord client ID not configured" }); } // Use the API base URL (should match Discord Dev Portal redirect URI) let apiBase = process.env.VITE_API_BASE || process.env.API_BASE || process.env.PUBLIC_BASE_URL || process.env.SITE_URL; if (!apiBase) { // Fallback to request origin if no env var is set const protocol = req.headers["x-forwarded-proto"] || req.protocol || "https"; const host = req.headers["x-forwarded-host"] || req.hostname || req.get("host"); apiBase = `${protocol}://${host}`; } const redirectUri = `${apiBase}/api/discord/oauth/callback`; console.log( "[Discord OAuth Start] Using redirect URI:", redirectUri, "from API base:", apiBase, ); // Get the state from query params (can be a JSON string with action and redirectTo) let state = req.query.state || "/dashboard"; if (typeof state !== "string") { state = "/dashboard"; } const params = new URLSearchParams({ client_id: clientId, redirect_uri: redirectUri, response_type: "code", scope: "identify email", state: state, }); const discordOAuthUrl = `https://discord.com/api/oauth2/authorize?${params.toString()}`; console.log("[Discord OAuth Start] Redirecting to:", discordOAuthUrl); return res.redirect(302, discordOAuthUrl); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // Discord OAuth: callback handler app.get("/api/discord/oauth/callback", async (req, res) => { const code = req.query.code as string; const state = req.query.state as string; const error = req.query.error as string; if (error) { return res.redirect(`/login?error=${error}`); } if (!code) { return res.redirect("/login?error=no_code"); } // Parse state to determine if this is a linking or login flow let isLinkingFlow = false; let redirectTo = "/dashboard"; let authenticatedUserId: string | null = null; if (state) { try { const stateData = JSON.parse(decodeURIComponent(state)); isLinkingFlow = stateData.action === "link"; redirectTo = stateData.redirectTo || redirectTo; if (isLinkingFlow && stateData.sessionToken) { // Look up the linking session to get the user ID const { data: session, error: sessionError } = await adminSupabase .from("discord_linking_sessions") .select("user_id") .eq("session_token", stateData.sessionToken) .gt("expires_at", new Date().toISOString()) .single(); if (sessionError || !session) { console.error( "[Discord OAuth] Linking session not found or expired", ); return res.redirect( "/login?error=session_lost&message=Session%20expired.%20Please%20try%20linking%20Discord%20again.", ); } authenticatedUserId = session.user_id; console.log( "[Discord OAuth] Linking session found, user_id:", authenticatedUserId, ); // Clean up the temporary session await adminSupabase .from("discord_linking_sessions") .delete() .eq("session_token", stateData.sessionToken); } } catch (e) { console.log("[Discord OAuth] Could not parse state:", e); } } try { const clientId = process.env.DISCORD_CLIENT_ID; const clientSecret = process.env.DISCORD_CLIENT_SECRET; // Use the same redirect URI as the start endpoint let apiBase = process.env.VITE_API_BASE || process.env.API_BASE || process.env.PUBLIC_BASE_URL || process.env.SITE_URL; if (!apiBase) { // Fallback to request origin if no env var is set const protocol = req.headers["x-forwarded-proto"] || req.protocol || "https"; const host = req.headers["x-forwarded-host"] || req.hostname || req.get("host"); apiBase = `${protocol}://${host}`; } const redirectUri = `${apiBase}/api/discord/oauth/callback`; console.log( "[Discord OAuth Callback] Received callback, redirect URI:", redirectUri, ); if (!clientId || !clientSecret) { console.error("[Discord OAuth] Missing client credentials"); return res.redirect("/login?error=config"); } // Exchange authorization code for access token const tokenResponse = await fetch( "https://discord.com/api/oauth2/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ client_id: clientId, client_secret: clientSecret, code, grant_type: "authorization_code", redirect_uri: redirectUri, }).toString(), }, ); if (!tokenResponse.ok) { const errorData = await tokenResponse.json(); console.error("[Discord OAuth] Token exchange failed:", errorData); return res.redirect("/login?error=token_exchange"); } const tokenData = await tokenResponse.json(); // Get Discord user information const userResponse = await fetch( "https://discord.com/api/v10/users/@me", { headers: { Authorization: `Bearer ${tokenData.access_token}`, }, }, ); if (!userResponse.ok) { console.error( "[Discord OAuth] User fetch failed:", userResponse.status, ); return res.redirect("/login?error=user_fetch"); } const discordUser = await userResponse.json(); if (!discordUser.email) { console.error("[Discord OAuth] Discord user has no email"); return res.redirect( "/login?error=no_email&message=Please+enable+email+on+your+Discord+account", ); } // LINKING FLOW: Link Discord to authenticated user if (isLinkingFlow && authenticatedUserId) { console.log( "[Discord OAuth] Linking Discord to user:", authenticatedUserId, ); // Check if Discord ID is already linked to someone else const { data: existingLink } = await adminSupabase .from("discord_links") .select("user_id") .eq("discord_id", discordUser.id) .single(); if (existingLink && existingLink.user_id !== authenticatedUserId) { console.error( "[Discord OAuth] Discord ID already linked to different user", ); return res.redirect( `/dashboard?error=already_linked&message=${encodeURIComponent("This Discord account is already linked to another AeThex account")}`, ); } // Create or update Discord link const { error: linkError } = await adminSupabase .from("discord_links") .upsert({ discord_id: discordUser.id, user_id: authenticatedUserId, linked_at: new Date().toISOString(), }); if (linkError) { console.error("[Discord OAuth] Link creation failed:", linkError); return res.redirect( `/dashboard?error=link_failed&message=${encodeURIComponent("Failed to link Discord account")}`, ); } console.log( "[Discord OAuth] Successfully linked Discord:", discordUser.id, ); return res.redirect(redirectTo); } // LOGIN FLOW: Check if Discord user already exists const { data: existingLink } = await adminSupabase .from("discord_links") .select("user_id") .eq("discord_id", discordUser.id) .single(); let userId: string; if (existingLink) { // Discord ID already linked - use existing user userId = existingLink.user_id; console.log( "[Discord OAuth] Discord ID already linked to user:", userId, ); } else { // Check if email matches existing account const { data: existingUserProfile } = await adminSupabase .from("user_profiles") .select("id") .eq("email", discordUser.email) .single(); if (existingUserProfile) { // Discord email matches existing user profile - link it userId = existingUserProfile.id; console.log( "[Discord OAuth] Discord email matches existing user profile, linking Discord", ); } else { // Discord email doesn't match any existing account // Don't auto-create - ask user to sign in with email first console.log( "[Discord OAuth] Discord email not found in existing accounts, redirecting to sign in", ); return res.redirect( `/login?error=discord_no_match&message=${encodeURIComponent(`Discord email (${discordUser.email}) not found. Please sign in with your email account first, then link Discord from settings.`)}`, ); } } // Create Discord link const { error: linkError } = await adminSupabase .from("discord_links") .upsert({ discord_id: discordUser.id, user_id: userId, linked_at: new Date().toISOString(), }); if (linkError) { console.error("[Discord OAuth] Link creation failed:", linkError); return res.redirect("/login?error=link_create"); } // Discord is now linked! Redirect to login for user to sign in console.log( "[Discord OAuth] Discord linked successfully, redirecting to login", ); return res.redirect( `/login?discord_linked=true&email=${encodeURIComponent(discordUser.email)}`, ); } catch (e: any) { console.error("[Discord OAuth] Callback error:", e); return res.redirect("/login?error=unknown"); } }); // Discord Create Linking Session: Creates temporary session for OAuth linking app.post("/api/discord/create-linking-session", async (req, res) => { try { const authHeader = req.headers.authorization; if (!authHeader) { return res.status(401).json({ error: "Not authenticated" }); } const token = authHeader.replace("Bearer ", ""); const { data: { user }, error, } = await adminSupabase.auth.getUser(token); if (error || !user) { return res.status(401).json({ error: "Invalid auth token" }); } const sessionToken = randomBytes(32).toString("hex"); const expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString(); const { error: insertError } = await adminSupabase .from("discord_linking_sessions") .insert({ user_id: user.id, session_token: sessionToken, expires_at: expiresAt, }); if (insertError) { console.error("[Discord] Session insert error:", { code: insertError.code, message: insertError.message, details: insertError.details, hint: insertError.hint, }); return res.status(500).json({ error: insertError.message, details: insertError.details, }); } res.json({ token: sessionToken }); } catch (error: any) { console.error("[Discord] Create session error:", error); res.status(500).json({ error: error.message }); } }); // Discord Verify Code: Link account using verification code app.post("/api/discord/verify-code", async (req, res) => { const { verification_code, user_id } = req.body || {}; console.log("[Discord Verify] Request received:", { verification_code: verification_code ? "***" : "missing", user_id: user_id || "missing", }); if (!verification_code || !user_id) { console.error("[Discord Verify] Missing params:", { hasCode: !!verification_code, hasUserId: !!user_id, }); return res.status(400).json({ message: "Missing verification code or user ID", }); } try { // Cleanup expired verification codes (opportunistic cleanup) adminSupabase .from("discord_verifications") .delete() .lt("expires_at", new Date().toISOString()) .then(({ error }) => { if (error) console.warn("[Discord Verify] Cleanup error:", error.message); }); // Find valid verification code const { data: verification, error: verifyError } = await adminSupabase .from("discord_verifications") .select("*") .eq("verification_code", verification_code.trim()) .gt("expires_at", new Date().toISOString()) .single(); if (verifyError) { console.error( "[Discord Verify] Error querying verification code:", verifyError, ); return res.status(400).json({ message: "Invalid or expired verification code. Please try /verify again.", }); } if (!verification) { console.warn( "[Discord Verify] Verification code not found or expired:", verification_code, ); return res.status(400).json({ message: "Invalid or expired verification code. Please try /verify again.", }); } const discordId = verification.discord_id; console.log( "[Discord Verify] Found verification code for Discord ID:", { discordId, userId: user_id, }, ); // Check if already linked const { data: existingLink, error: linkCheckError } = await adminSupabase .from("discord_links") .select("*") .eq("discord_id", discordId) .single(); if (linkCheckError && linkCheckError.code !== "PGRST116") { console.error( "[Discord Verify] Error checking existing link:", linkCheckError, ); } if (existingLink && existingLink.user_id !== user_id) { console.warn( "[Discord Verify] Discord ID already linked to different user:", { discordId, existingUserId: existingLink.user_id, newUserId: user_id, }, ); return res.status(400).json({ message: "This Discord account is already linked to another AeThex account.", }); } // Create or update link const { error: linkError } = await adminSupabase .from("discord_links") .upsert({ discord_id: discordId, user_id: user_id, linked_at: new Date().toISOString(), }); if (linkError) { console.error("[Discord Verify] Link creation failed:", linkError); return res.status(500).json({ message: "Failed to link Discord account: " + linkError.message, }); } console.log("[Discord Verify] Link created successfully:", { discordId, userId: user_id, }); // Delete used verification code await adminSupabase .from("discord_verifications") .delete() .eq("verification_code", verification_code.trim()); // Notify bot about successful verification (fire and forget) const botWebhookUrl = process.env.DISCORD_BOT_WEBHOOK_URL || "https://aethex-bot-master.replit.app/api/verify-success"; fetch(botWebhookUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ discord_id: discordId, user_id: user_id, success: true, timestamp: new Date().toISOString(), }), }).catch((err) => { console.warn("[Discord Verify] Failed to notify bot:", err.message); }); res.status(200).json({ success: true, message: "Discord account linked successfully!", discord_user: { id: discordId, username: verification.username || "Discord User", }, }); } catch (error: any) { console.error("[Discord Verify] Unexpected error:", error); res.status(500).json({ message: "An error occurred. Please try again: " + error?.message, }); } }); // Discord Verify Callback: Webhook for bot to receive verification confirmation // Bot calls this after aethex.dev successfully links the Discord account app.post("/api/discord/verify-callback", async (req, res) => { const { discord_id, user_id, success, bot_secret } = req.body || {}; // Require environment secret - no fallback for security const expectedSecret = process.env.DISCORD_BOT_WEBHOOK_SECRET; if (!expectedSecret) { console.error("[Discord Callback] DISCORD_BOT_WEBHOOK_SECRET not configured"); return res.status(503).json({ error: "Service not configured" }); } if (!bot_secret || bot_secret !== expectedSecret) { console.warn("[Discord Callback] Invalid or missing bot secret"); return res.status(403).json({ error: "Invalid authorization" }); } if (!discord_id) { return res.status(400).json({ error: "Missing discord_id" }); } console.log("[Discord Callback] Verification callback received:", { discord_id, user_id: user_id || "not provided", success, }); // This endpoint exists for the bot to know verification succeeded // The bot can then assign the Verified role in Discord res.status(200).json({ received: true, discord_id, message: success ? "Verification confirmed" : "Verification status received", }); }); // Discord Cleanup: Remove expired verification codes (called periodically or on verify-code) app.post("/api/discord/cleanup-expired-codes", async (req, res) => { try { const { deleted, error } = await adminSupabase .from("discord_verifications") .delete() .lt("expires_at", new Date().toISOString()) .select(); if (error) { console.error("[Discord Cleanup] Error cleaning expired codes:", error); return res.status(500).json({ error: error.message }); } const count = deleted?.length || 0; console.log(`[Discord Cleanup] Removed ${count} expired verification codes`); res.status(200).json({ success: true, removed: count }); } catch (error: any) { console.error("[Discord Cleanup] Unexpected error:", error); res.status(500).json({ error: error.message }); } }); // Discord Role Mappings CRUD app.get("/api/discord/role-mappings", async (req, res) => { try { const { data, error } = await adminSupabase .from("discord_role_mappings") .select("*") .order("created_at", { ascending: false }); if (error) { console.error("[Discord] Error fetching role mappings:", error); return res.status(500).json({ error: `Failed to fetch role mappings: ${error.message}`, }); } res.setHeader("Content-Type", "application/json"); return res.json(data || []); } catch (e: any) { console.error("[Discord] Exception fetching role mappings:", e); res.setHeader("Content-Type", "application/json"); return res.status(500).json({ error: e?.message || "Failed to fetch role mappings", }); } }); // Discord Activity Token Exchange Endpoint app.post("/api/discord/token", async (req, res) => { try { const { code } = req.body; if (!code) { return res.status(400).json({ error: "Missing authorization code" }); } const clientId = process.env.DISCORD_CLIENT_ID; const clientSecret = process.env.DISCORD_CLIENT_SECRET; if (!clientId || !clientSecret) { console.error("[Discord Token] Missing CLIENT_ID or CLIENT_SECRET"); return res.status(500).json({ error: "Server not configured" }); } // Exchange authorization code for access token const tokenResponse = await fetch( "https://discord.com/api/v10/oauth2/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ client_id: clientId, client_secret: clientSecret, grant_type: "authorization_code", code, redirect_uri: process.env.DISCORD_ACTIVITY_REDIRECT_URI || "https://aethex.dev/activity", }).toString(), }, ); if (!tokenResponse.ok) { const error = await tokenResponse.json(); console.error("[Discord Token] Token exchange failed:", error); return res.status(400).json({ error: "Failed to exchange code for token", details: error, }); } const tokenData = await tokenResponse.json(); const accessToken = tokenData.access_token; if (!accessToken) { console.error("[Discord Token] No access token in response"); return res .status(500) .json({ error: "Failed to obtain access token" }); } // Fetch Discord user info to ensure token is valid const userResponse = await fetch( "https://discord.com/api/v10/users/@me", { headers: { Authorization: `Bearer ${accessToken}`, }, }, ); if (!userResponse.ok) { console.error("[Discord Token] Failed to fetch user info"); return res.status(401).json({ error: "Invalid token" }); } const discordUser = await userResponse.json(); console.log( "[Discord Token] Token exchange successful for user:", discordUser.id, ); // Return access token to Activity return res.status(200).json({ access_token: accessToken, token_type: tokenData.token_type, expires_in: tokenData.expires_in, user_id: discordUser.id, username: discordUser.username, }); } catch (error) { console.error("[Discord Token] Error:", error); return res.status(500).json({ error: "Internal server error", message: error instanceof Error ? error.message : String(error), }); } }); app.post("/api/discord/role-mappings", async (req, res) => { try { const { arm, discord_role, discord_role_name, server_id, user_type } = req.body; const roleName = discord_role_name || discord_role; if (!arm || !roleName) { return res.status(400).json({ error: "arm and discord_role (or discord_role_name) are required", }); } const { data, error } = await adminSupabase .from("discord_role_mappings") .insert({ arm, user_type: user_type || "community_member", discord_role_name: roleName, server_id: server_id || null, }) .select() .single(); if (error) { console.error("[Discord] Error creating role mapping:", error); return res.status(500).json({ error: `Failed to create mapping: ${error.message}`, }); } res.setHeader("Content-Type", "application/json"); return res.status(201).json(data); } catch (e: any) { console.error("[Discord] Exception creating role mapping:", e); res.setHeader("Content-Type", "application/json"); return res.status(500).json({ error: e?.message || "Failed to create mapping", }); } }); app.put("/api/discord/role-mappings", async (req, res) => { try { const { id, arm, discord_role, discord_role_name, server_id, user_type, } = req.body; if (!id) { return res.status(400).json({ error: "id is required" }); } const updateData: any = {}; if (arm) updateData.arm = arm; const roleName = discord_role_name || discord_role; if (roleName) updateData.discord_role_name = roleName; if (server_id !== undefined) updateData.server_id = server_id; if (user_type) updateData.user_type = user_type; const { data, error } = await adminSupabase .from("discord_role_mappings") .update(updateData) .eq("id", id) .select() .single(); if (error) { console.error("[Discord] Error updating role mapping:", error); return res.status(500).json({ error: `Failed to update mapping: ${error.message}`, }); } res.setHeader("Content-Type", "application/json"); return res.json(data); } catch (e: any) { console.error("[Discord] Exception updating role mapping:", e); res.setHeader("Content-Type", "application/json"); return res.status(500).json({ error: e?.message || "Failed to update mapping", }); } }); app.delete("/api/discord/role-mappings", async (req, res) => { try { const { id } = req.query; if (!id) { return res.status(400).json({ error: "id query parameter is required", }); } const { error } = await adminSupabase .from("discord_role_mappings") .delete() .eq("id", id); if (error) { console.error("[Discord] Error deleting role mapping:", error); return res.status(500).json({ error: `Failed to delete mapping: ${error.message}`, }); } res.setHeader("Content-Type", "application/json"); return res.json({ success: true }); } catch (e: any) { console.error("[Discord] Exception deleting role mapping:", e); res.setHeader("Content-Type", "application/json"); return res.status(500).json({ error: e?.message || "Failed to delete mapping", }); } }); // Discord Admin Register Commands app.get("/api/discord/admin-register-commands", async (req, res) => { // GET: Show HTML form for browser access res.setHeader("Content-Type", "text/html; charset=utf-8"); return res.send(` Register Discord Commands

��� Discord Commands Registration

Register all Discord slash commands for AeThex

Commands to be registered:
  • āœ… /verify - Link your Discord account
  • āœ… /set-realm - Choose your primary arm
  • āœ… /profile - View your AeThex profile
  • āœ… /unlink - Disconnect your account
  • āœ… /verify-role - Check your Discord roles
ā³ Registering commands... please wait...
`); }); app.post("/api/discord/admin-register-commands", async (req, res) => { try { // Skip auth for localhost/development const isLocalhost = req.hostname === "localhost" || req.hostname === "127.0.0.1"; if (!isLocalhost) { const authHeader = req.headers.authorization; const tokenFromBody = req.body?.token as string; // Extract token from Bearer header let token = null; if (authHeader && authHeader.startsWith("Bearer ")) { token = authHeader.substring(7); } else if (tokenFromBody) { token = tokenFromBody; } const adminToken = process.env.DISCORD_ADMIN_REGISTER_TOKEN; if (!adminToken || !token || token !== adminToken) { console.error( "[Discord] Authorization failed - token mismatch or missing", ); return res.status(401).json({ error: "Unauthorized - invalid or missing admin token", }); } } const botToken = process.env.DISCORD_BOT_TOKEN?.trim(); const clientId = process.env.DISCORD_CLIENT_ID?.trim(); console.log( "[Discord] Config check - botToken set:", !!botToken, "clientId set:", !!clientId, "botToken length:", botToken?.length, ); // Log first and last few chars of token for debugging (safe logging) if (botToken) { const first = botToken.substring(0, 5); const last = botToken.substring(botToken.length - 5); console.log( "[Discord] Token preview: " + first + "..." + last, "Total length:", botToken.length, ); } if (!botToken || !clientId) { return res.status(500).json({ error: "Discord bot token or client ID not configured", }); } // Validate token format if (botToken.length < 20) { console.warn( "[Discord] Bot token appears invalid - length:", botToken.length, ); return res.status(500).json({ error: "Discord bot token appears invalid (check DISCORD_BOT_TOKEN in environment)", tokenLength: botToken.length, }); } // Register slash commands const commands = [ { name: "verify", description: "Link your Discord account to AeThex", type: 1, }, { name: "set-realm", description: "Choose your primary arm/realm", type: 1, options: [ { name: "realm", description: "Select your primary realm", type: 3, required: true, choices: [ { name: "Labs", value: "labs" }, { name: "GameForge", value: "gameforge" }, { name: "Corp", value: "corp" }, { name: "Foundation", value: "foundation" }, { name: "Nexus", value: "nexus" }, ], }, ], }, { name: "profile", description: "View your AeThex profile", type: 1, }, { name: "unlink", description: "Disconnect your Discord account from AeThex", type: 1, }, { name: "verify-role", description: "Check your assigned Discord roles", type: 1, }, ]; const registerUrl = `https://discord.com/api/v10/applications/${clientId}/commands`; console.log("[Discord] Calling Discord API:", registerUrl); console.log("[Discord] Bot token length:", botToken.length); console.log("[Discord] Sending", commands.length, "commands"); const response = await fetch(registerUrl, { method: "PUT", headers: { Authorization: `Bot ${botToken}`, "Content-Type": "application/json", }, body: JSON.stringify(commands), }); console.log("[Discord] Discord API response status:", response.status); if (!response.ok) { const errorData = await response.text(); let errorJson = {}; try { errorJson = JSON.parse(errorData); } catch {} console.error( "[Discord] Command registration failed:", response.status, errorData, ); return res.status(500).json({ error: `Discord API error (${response.status}): ${errorData}`, discordError: errorJson, }); } const result = await response.json(); console.log( "[Discord] Commands registered successfully:", result.length, ); res.setHeader("Content-Type", "application/json"); return res.json({ ok: true, message: `Registered ${result.length} commands`, commands: result, }); } catch (e: any) { console.error( "[Discord] Exception registering commands:", e.message, e.stack, ); res.setHeader("Content-Type", "application/json"); return res.status(500).json({ error: e?.message || "Failed to register commands", details: process.env.NODE_ENV === "development" ? e?.stack : undefined, }); } }); // Discord Bot Health Check (proxy to avoid CSP issues) // Set DISCORD_BOT_HEALTH_URL env var to bot's actual health endpoint // Examples: // - Railway internal: http://aethex.railway.internal:8044/health // - External URL: https://bot.example.com/health // - Local: http://localhost:3000/health app.get("/api/discord/bot-health", async (req, res) => { try { // Try multiple bot health URLs in order of preference const botHealthUrls = [ process.env.DISCORD_BOT_HEALTH_URL, "http://aethex.railway.internal:8044/health", // Railway internal network "http://localhost:8044/health", // Local fallback ].filter(Boolean) as string[]; let lastError: Error | null = null; let response: Response | null = null; for (const url of botHealthUrls) { try { console.log(`[Discord Bot Health] Attempting to reach: ${url}`); // Create AbortController with 5 second timeout for internal Railway, 3 seconds for localhost const isInternal = url.includes("railway.internal"); const timeoutMs = isInternal ? 5000 : 3000; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { response = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json", }, signal: controller.signal, }); } finally { clearTimeout(timeoutId); } const contentType = response.headers.get("content-type") || ""; const responseBody = await response.text(); console.log( `[Discord Bot Health] Response from ${url}: Status ${response.status}, Content-Type: ${contentType}, Body: ${responseBody.substring(0, 200)}`, ); if (response.ok && contentType.includes("application/json")) { console.log(`[Discord Bot Health] Success from ${url}`); const data = JSON.parse(responseBody); res.setHeader("Content-Type", "application/json"); return res.json({ status: data.status || "online", guilds: data.guilds || 0, commands: data.commands || 0, uptime: data.uptime || 0, timestamp: data.timestamp || new Date().toISOString(), }); } if (response.ok && !contentType.includes("application/json")) { lastError = new Error( `Got non-JSON response (${contentType}): ${responseBody.substring(0, 100)}`, ); continue; } } catch (err) { lastError = err instanceof Error ? err : new Error(String(err)); console.warn( `[Discord Bot Health] Failed to reach ${url}: ${lastError.message}`, ); continue; } } // Could not reach any health endpoint res.setHeader("Content-Type", "application/json"); return res.status(503).json({ status: "offline", error: `Could not reach bot health endpoint. Last error: ${lastError?.message || "Unknown error"}`, }); } catch (error: any) { console.error("[Discord Bot Health] Error:", error); res.setHeader("Content-Type", "application/json"); return res.status(500).json({ status: "offline", error: error instanceof Error ? error.message : "Failed to reach bot health endpoint", }); } }); // Bot Management Proxy Endpoints (session-authenticated) const BOT_ADMIN_TOKEN = process.env.DISCORD_ADMIN_TOKEN || "aethex-bot-admin"; const getBotApiUrl = () => { const urls = [ process.env.DISCORD_BOT_HEALTH_URL?.replace("/health", ""), "http://localhost:8044", ].filter(Boolean); return urls[0] || "http://localhost:8044"; }; // Proxy to bot-status app.get("/api/discord/bot-status", async (req, res) => { try { const botUrl = getBotApiUrl(); const response = await fetch(`${botUrl}/bot-status`, { headers: { Authorization: `Bearer ${BOT_ADMIN_TOKEN}` }, }); if (!response.ok) throw new Error(`Bot returned ${response.status}`); const data = await response.json(); res.json(data); } catch (error: any) { res.status(503).json({ error: error.message, status: "offline" }); } }); // Proxy to linked-users app.get("/api/discord/linked-users", async (req, res) => { try { const botUrl = getBotApiUrl(); const response = await fetch(`${botUrl}/linked-users`, { headers: { Authorization: `Bearer ${BOT_ADMIN_TOKEN}` }, }); if (!response.ok) throw new Error(`Bot returned ${response.status}`); const data = await response.json(); res.json(data); } catch (error: any) { res.status(503).json({ error: error.message, success: false }); } }); // Proxy to command-stats app.get("/api/discord/command-stats", async (req, res) => { try { const botUrl = getBotApiUrl(); const response = await fetch(`${botUrl}/command-stats`, { headers: { Authorization: `Bearer ${BOT_ADMIN_TOKEN}` }, }); if (!response.ok) throw new Error(`Bot returned ${response.status}`); const data = await response.json(); res.json(data); } catch (error: any) { res.status(503).json({ error: error.message, success: false }); } }); // Proxy to feed-stats app.get("/api/discord/feed-stats", async (req, res) => { try { const botUrl = getBotApiUrl(); const response = await fetch(`${botUrl}/feed-stats`, { headers: { Authorization: `Bearer ${BOT_ADMIN_TOKEN}` }, }); if (!response.ok) throw new Error(`Bot returned ${response.status}`); const data = await response.json(); res.json(data); } catch (error: any) { res.status(503).json({ error: error.message, success: false }); } }); // Proxy to register-commands app.post("/api/discord/bot-register-commands", async (req, res) => { try { const botUrl = getBotApiUrl(); const response = await fetch(`${botUrl}/register-commands`, { method: "POST", headers: { Authorization: `Bearer ${BOT_ADMIN_TOKEN}`, "Content-Type": "application/json", }, }); if (!response.ok) throw new Error(`Bot returned ${response.status}`); const data = await response.json(); res.json(data); } catch (error: any) { res.status(503).json({ error: error.message, success: false }); } }); // Discord Token Diagnostic Endpoint app.get("/api/discord/diagnostic", async (req, res) => { try { const botToken = process.env.DISCORD_BOT_TOKEN?.trim(); const clientId = process.env.DISCORD_CLIENT_ID?.trim(); const publicKey = process.env.DISCORD_PUBLIC_KEY?.trim(); const diagnostics = { timestamp: new Date().toISOString(), environment: { botTokenSet: !!botToken, clientIdSet: !!clientId, publicKeySet: !!publicKey, }, tokenValidation: { length: botToken?.length || 0, format: botToken ? "valid_format" : "missing", preview: botToken ? `${botToken.substring(0, 15)}...${botToken.substring(botToken.length - 10)}` : null, minLengthMet: (botToken?.length || 0) >= 20, }, clientIdValidation: { value: clientId || null, isNumeric: /^\d+$/.test(clientId || ""), }, testRequest: { url: `https://discord.com/api/v10/applications/${clientId}/commands`, method: "PUT", headerFormat: "Bot {token}", status: "ready_to_test", }, recommendations: [] as string[], }; // Add recommendations based on validation if (!botToken) { diagnostics.recommendations.push( "āŒ DISCORD_BOT_TOKEN not set. Set it in environment variables.", ); } else if ((botToken?.length || 0) < 20) { diagnostics.recommendations.push( `āŒ DISCORD_BOT_TOKEN appears invalid (length: ${botToken?.length}). Should be 60+ characters.`, ); } else { diagnostics.recommendations.push( "āœ… DISCORD_BOT_TOKEN format looks valid", ); } if (!clientId) { diagnostics.recommendations.push( "�� DISCORD_CLIENT_ID not set. Set it to your application's ID.", ); } else { diagnostics.recommendations.push("�� DISCORD_CLIENT_ID is set"); } if (!publicKey) { diagnostics.recommendations.push( "��� DISCORD_PUBLIC_KEY not set. Needed for signature verification.", ); } else { diagnostics.recommendations.push("āœ… DISCORD_PUBLIC_KEY is set"); } // Test if token works with Discord API if (botToken && clientId) { try { const testResponse = await fetch( `https://discord.com/api/v10/applications/${clientId}`, { headers: { Authorization: `Bot ${botToken}`, }, }, ); diagnostics.testRequest = { ...diagnostics.testRequest, status: testResponse.status === 200 ? "āœ… Success" : `āŒ Failed (${testResponse.status})`, responseCode: testResponse.status, }; if (testResponse.status === 401) { diagnostics.recommendations.push( "āŒ Token authentication failed (401). The token may be invalid or revoked.", ); } else if (testResponse.status === 200) { diagnostics.recommendations.push( "āœ… Token authentication successful with Discord API!", ); } } catch (error) { diagnostics.testRequest = { ...diagnostics.testRequest, status: "āš ļø Network Error", error: error instanceof Error ? error.message : "Unknown error", }; } } res.setHeader("Content-Type", "application/json"); return res.json(diagnostics); } catch (error: any) { console.error("[Discord Diagnostic] Error:", error); res.setHeader("Content-Type", "application/json"); return res.status(500).json({ error: error instanceof Error ? error.message : "Diagnostic failed", }); } }); // ===== DEVELOPER API ENDPOINTS ===== // Developer API Keys Management app.get("/api/developer/keys", listKeys); app.post("/api/developer/keys", createKey); app.delete("/api/developer/keys/:id", deleteKey); app.patch("/api/developer/keys/:id", updateKey); app.get("/api/developer/keys/:id/stats", getKeyStats); // Developer Profile Management app.get("/api/developer/profile", getProfile); app.patch("/api/developer/profile", updateProfile); // ===== END DEVELOPER API ENDPOINTS ===== // Site settings (admin-managed) app.get("/api/site-settings", async (req, res) => { try { const key = String(req.query.key || "").trim(); if (key) { try { const { data, error } = await adminSupabase .from("site_settings") .select("value") .eq("key", key) .maybeSingle(); if (error) { if (isTableMissing(error)) return res.json({}); return res.status(500).json({ error: error.message }); } return res.json((data as any)?.value || {}); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } } const { data, error } = await adminSupabase .from("site_settings") .select("key, value"); if (error) { if (isTableMissing(error)) return res.json({}); return res.status(500).json({ error: error.message }); } const map: Record = {}; for (const row of data || []) map[(row as any).key] = (row as any).value; return res.json(map); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/site-settings", async (req, res) => { try { const { key, value } = (req.body || {}) as { key?: string; value?: any; }; if (!key || typeof key !== "string") { return res.status(400).json({ error: "key required" }); } const payload = { key, value: value ?? {} } as any; const { error } = await adminSupabase .from("site_settings") .upsert(payload, { onConflict: "key" as any }); if (error) { if (isTableMissing(error)) return res .status(400) .json({ error: "site_settings table missing" }); return res.status(500).json({ error: error.message }); } return res.json({ ok: true }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); app.get("/api/health", async (_req, res) => { try { const { error } = await adminSupabase .from("user_profiles") .select("count", { count: "exact", head: true }); if (error) return res.status(500).json({ ok: false, error: error.message }); return res.json({ ok: true }); } catch (e: any) { return res .status(500) .json({ ok: false, error: e?.message || String(e) }); } }); app.get("/api/posts", async (req, res) => { const limit = Math.max(1, Math.min(50, Number(req.query.limit) || 10)); try { const { data, error } = await adminSupabase .from("community_posts") .select(`*, user_profiles ( username, full_name, avatar_url )`) .eq("is_published", true) .order("created_at", { ascending: false }) .limit(limit); if (error) return res.status(500).json({ error: error.message }); res.json(data || []); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.get("/api/user/:id/posts", async (req, res) => { const userId = req.params.id; try { const { data, error } = await adminSupabase .from("community_posts") .select("*") .eq("author_id", userId) .order("created_at", { ascending: false }); if (error) return res.status(500).json({ error: error.message }); res.json(data || []); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/posts", async (req, res) => { console.log("[API] POST /api/posts called"); const payload = req.body || {}; console.log("[API] Payload:", JSON.stringify(payload).slice(0, 200)); // Validation if (!payload.author_id) { console.log("[API] Error: author_id is required"); return res.status(400).json({ error: "author_id is required" }); } if ( !payload.title || typeof payload.title !== "string" || !payload.title.trim() ) { return res .status(400) .json({ error: "title is required and must be a non-empty string" }); } if ( !payload.content || typeof payload.content !== "string" || !payload.content.trim() ) { return res.status(400).json({ error: "content is required and must be a non-empty string", }); } // Validate author_id is a valid UUID format const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (!uuidRegex.test(String(payload.author_id))) { return res .status(400) .json({ error: "author_id must be a valid UUID" }); } try { // Verify author exists const { data: author, error: authorError } = await adminSupabase .from("user_profiles") .select("id") .eq("id", payload.author_id) .single(); if (authorError || !author) { return res.status(404).json({ error: "Author not found" }); } const { data, error } = await adminSupabase .from("community_posts") .insert({ author_id: payload.author_id, title: String(payload.title).trim(), content: String(payload.content).trim(), category: payload.category ? String(payload.category).trim() : null, tags: Array.isArray(payload.tags) ? payload.tags.map((t: any) => String(t).trim()) : [], is_published: payload.is_published ?? true, }) .select() .single(); if (error) { console.error("[API] /api/posts insert error:", { code: error.code, message: error.message, details: (error as any).details, }); return res .status(500) .json({ error: error.message || "Failed to create post" }); } console.log("[API] Post created successfully:", data?.id); res.json(data); } catch (e: any) { console.error("[API] /api/posts exception:", e?.message || String(e)); res.status(500).json({ error: e?.message || "Failed to create post" }); } }); app.post("/api/profile/ensure", async (req, res) => { const { id, profile } = req.body || {}; console.log("[API] /api/profile/ensure called", { id, profile }); if (!id) return res.status(400).json({ error: "missing id" }); const tryUpsert = async (payload: any) => { const resp = await adminSupabase .from("user_profiles") .upsert(payload, { onConflict: "id" }) .select() .single(); return resp as any; }; try { let username = profile?.username; let attempt = await tryUpsert({ id, ...profile, username }); const normalizeError = (err: any) => { if (!err) return null; if (typeof err === "string") return { message: err }; if (typeof err === "object" && Object.keys(err).length === 0) return null; // treat empty object as no error return err; }; let error = normalizeError(attempt.error); if (error) { console.error("[API] ensure upsert error:", { message: (error as any).message, code: (error as any).code, details: (error as any).details, hint: (error as any).hint, }); const message: string = (error as any).message || ""; const code: string = (error as any).code || ""; // Handle duplicate username if ( code === "23505" || message.includes("duplicate key") || message.includes("username") ) { const suffix = Math.random().toString(36).slice(2, 6); const newUsername = `${String(username || "user").slice(0, 20)}_${suffix}`; console.log("[API] retrying with unique username", newUsername); attempt = await tryUpsert({ id, ...profile, username: newUsername, }); error = normalizeError(attempt.error); } } if (error) { // Possible foreign key violation: auth.users missing if ( (error as any).code === "23503" || (error as any).message?.includes("foreign key") ) { return res.status(400).json({ error: "User does not exist in authentication system. Please sign out and sign back in, then retry onboarding.", }); } return res.status(500).json({ error: (error as any).message || JSON.stringify(error) || "Unknown error", }); } return res.json(attempt.data || {}); } catch (e: any) { console.error("[API] /api/profile/ensure exception:", e); res.status(500).json({ error: e?.message || String(e) }); } }); // Profile update endpoint - used by Dashboard realm/settings app.patch("/api/profile/update", async (req, res) => { // Authenticate user via Bearer token const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith("Bearer ")) { return res.status(401).json({ error: "Authentication required" }); } const token = authHeader.replace("Bearer ", ""); const { data: { user: authUser }, error: authError } = await adminSupabase.auth.getUser(token); if (authError || !authUser) { return res.status(401).json({ error: "Invalid or expired auth token" }); } const { user_id, ...updates } = req.body || {}; // Ensure user can only update their own profile if (user_id && user_id !== authUser.id) { return res.status(403).json({ error: "Cannot update another user's profile" }); } const targetUserId = user_id || authUser.id; // Whitelist allowed fields for security const allowedFields = [ "full_name", "bio", "avatar_url", "banner_url", "location", "website_url", "github_url", "linkedin_url", "twitter_url", "primary_realm", "experience_level", "user_type", ]; const sanitizedUpdates: Record = {}; for (const key of allowedFields) { if (key in updates) { sanitizedUpdates[key] = updates[key]; } } if (Object.keys(sanitizedUpdates).length === 0) { return res.status(400).json({ error: "No valid fields to update" }); } try { const { data, error } = await adminSupabase .from("user_profiles") .update({ ...sanitizedUpdates, updated_at: new Date().toISOString(), }) .eq("id", targetUserId) .select() .single(); if (error) { console.error("[Profile Update] Error:", error); return res.status(500).json({ error: error.message }); } return res.json(data); } catch (e: any) { console.error("[Profile Update] Exception:", e?.message); return res.status(500).json({ error: e?.message || "Failed to update profile" }); } }); // Wallet verification endpoint for Phase 2 Bridge UI app.post("/api/profile/wallet-verify", async (req, res) => { const { user_id, wallet_address } = req.body || {}; if (!user_id) { return res.status(400).json({ error: "user_id is required" }); } if (!wallet_address) { return res.status(400).json({ error: "wallet_address is required" }); } // Validate Ethereum address format (0x followed by 40 hex chars) const ethAddressRegex = /^0x[a-fA-F0-9]{40}$/; const normalizedAddress = String(wallet_address).toLowerCase(); if (!ethAddressRegex.test(normalizedAddress)) { return res.status(400).json({ error: "Invalid Ethereum address format", }); } try { // Check if wallet is already connected to a different user const { data: existingUser, error: checkError } = await adminSupabase .from("user_profiles") .select("id, username") .eq("wallet_address", normalizedAddress) .neq("id", user_id) .maybeSingle(); if (checkError && checkError.code !== "PGRST116") { // PGRST116 = no rows returned (expected) console.error("[Wallet Verify] Check error:", checkError); return res.status(500).json({ error: "Failed to verify wallet availability", }); } if (existingUser) { return res.status(409).json({ error: "This wallet is already connected to another account", }); } // Update user profile with wallet address const { data, error } = await adminSupabase .from("user_profiles") .update({ wallet_address: normalizedAddress }) .eq("id", user_id) .select() .single(); if (error) { console.error("[Wallet Verify] Update error:", error); return res.status(500).json({ error: error.message || "Failed to connect wallet", }); } console.log("[Wallet Verify] Wallet connected for user:", user_id); return res.json({ ok: true, message: "Wallet connected successfully", wallet_address: normalizedAddress, user: data, }); } catch (e: any) { console.error("[Wallet Verify] Exception:", e?.message); return res.status(500).json({ error: e?.message || "Failed to connect wallet", }); } }); // Wallet disconnection endpoint app.delete("/api/profile/wallet-verify", async (req, res) => { const { user_id } = req.body || {}; if (!user_id) { return res.status(400).json({ error: "user_id is required" }); } try { const { data, error } = await adminSupabase .from("user_profiles") .update({ wallet_address: null }) .eq("id", user_id) .select() .single(); if (error) { console.error("[Wallet Verify] Disconnect error:", error); return res.status(500).json({ error: error.message || "Failed to disconnect wallet", }); } console.log("[Wallet Verify] Wallet disconnected for user:", user_id); return res.json({ ok: true, message: "Wallet disconnected successfully", user: data, }); } catch (e: any) { console.error("[Wallet Verify] Disconnect exception:", e?.message); return res.status(500).json({ error: e?.message || "Failed to disconnect wallet", }); } }); app.post("/api/interests", async (req, res) => { const { user_id, interests } = req.body || {}; if (!user_id || !Array.isArray(interests)) return res.status(400).json({ error: "invalid payload" }); try { await adminSupabase .from("user_interests") .delete() .eq("user_id", user_id); if (interests.length) { const rows = interests.map((interest: string) => ({ user_id, interest, })); const { error } = await adminSupabase .from("user_interests") .insert(rows); if (error) return res.status(500).json({ error: error.message }); } res.json({ ok: true }); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.get("/api/featured-studios", async (_req, res) => { try { const { data, error } = await adminSupabase .from("featured_studios") .select("*") .order("rank", { ascending: true, nullsFirst: true } as any) .order("name", { ascending: true }); if (error) return res.status(500).json({ error: error.message }); res.json(data || []); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/featured-studios", async (req, res) => { const studios = (req.body?.studios || []) as any[]; if (!Array.isArray(studios)) return res.status(400).json({ error: "studios must be an array" }); try { const rows = studios.map((s: any, idx: number) => ({ id: s.id, name: String(s.name || "").trim(), tagline: s.tagline || null, metrics: s.metrics || null, specialties: Array.isArray(s.specialties) ? s.specialties : null, rank: Number.isFinite(s.rank) ? s.rank : idx, })); const { error } = await adminSupabase .from("featured_studios") .upsert(rows as any, { onConflict: "name" as any }); if (error) return res.status(500).json({ error: error.message }); res.json({ ok: true, count: rows.length }); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/achievements/award", async (req, res) => { const { user_id, achievement_names } = req.body || {}; if (!user_id) return res.status(400).json({ error: "user_id required" }); const names: string[] = Array.isArray(achievement_names) && achievement_names.length ? achievement_names : ["Welcome to AeThex"]; try { const { data: achievements, error: aErr } = await adminSupabase .from("achievements") .select("id, name") .in("name", names); if (aErr) return res.status(500).json({ error: aErr.message }); const rows = (achievements || []).map((a: any) => ({ user_id, achievement_id: a.id, })); if (!rows.length) return res.json({ ok: true, awarded: [] }); const { error: iErr } = await adminSupabase .from("user_achievements") .upsert(rows, { onConflict: "user_id,achievement_id" as any }); if (iErr && iErr.code !== "23505") return res.status(500).json({ error: iErr.message }); // Notify user of each achievement awarded if (rows.length) { const awardedNames = (achievements || []).map((a: any) => a.name).join(", "); await adminSupabase.from("notifications").insert({ user_id, type: "success", title: `šŸ† Achievement${rows.length > 1 ? "s" : ""} unlocked!`, message: awardedNames, }); } return res.json({ ok: true, awarded: rows.length }); } catch (e: any) { console.error("[API] achievements/award exception", e); return res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/achievements/activate", async (req, res) => { try { const { targetEmail, targetUsername } = req.body || {}; // Verify auth - get the requesting user from their token const authHeader = req.headers.authorization; let requestingUser: any = null; if (authHeader) { const token = authHeader.replace("Bearer ", ""); const { data: { user } } = await adminSupabase.auth.getUser(token); requestingUser = user; } // Get requester's profile to check if they're an admin let requesterProfile: any = null; if (requestingUser?.id) { const { data: profile } = await adminSupabase .from("user_profiles") .select("id, email, username, role") .eq("id", requestingUser.id) .single(); requesterProfile = profile; } // Security: If targeting someone other than yourself, you must be an admin const isAdmin = requesterProfile?.role === "admin" || requesterProfile?.role === "owner" || requesterProfile?.email === "mrpiglr@gmail.com"; const isSelfTarget = (targetEmail && requesterProfile?.email === targetEmail) || (targetUsername && requesterProfile?.username === targetUsername); // If not self-targeting and not admin, only allow seeding (no awards) const canAwardToTarget = isSelfTarget || isAdmin; const CORE_ACHIEVEMENTS = [ { id: "welcome-to-aethex", slug: "welcome-to-aethex", title: "Welcome to AeThex", description: "Completed onboarding and joined the AeThex network.", icon: "šŸŽ‰", xp_reward: 250, }, { id: "aethex-explorer", slug: "aethex-explorer", title: "AeThex Explorer", description: "Engaged with community initiatives and posted first update.", icon: "🧭", xp_reward: 400, }, { id: "community-champion", slug: "community-champion", title: "Community Champion", description: "Contributed feedback, resolved bugs, and mentored squads.", icon: "šŸ†", xp_reward: 750, }, { id: "workshop-architect", slug: "workshop-architect", title: "Workshop Architect", description: "Published a high-impact mod or toolkit adopted by teams.", icon: "šŸ› ļø", xp_reward: 1200, }, { id: "god-mode", slug: "god-mode", title: "GOD Mode", description: "Legendary status awarded by AeThex studio leadership.", icon: "⚔", xp_reward: 5000, }, ]; const generateDeterministicUUID = (str: string): string => { const hash = createHash("sha256").update(str).digest("hex"); return [ hash.slice(0, 8), hash.slice(8, 12), "5" + hash.slice(13, 16), ((parseInt(hash.slice(16, 18), 16) & 0x3f) | 0x80) .toString(16) .padStart(2, "0") + hash.slice(18, 20), hash.slice(20, 32), ].join("-"); }; // Step 1: Seed all core achievements const seededAchievements: { [key: string]: string } = {}; for (const achievement of CORE_ACHIEVEMENTS) { const uuidId = generateDeterministicUUID(achievement.id); const { error } = await adminSupabase.from("achievements").upsert( { id: uuidId, slug: achievement.slug, title: achievement.title, description: achievement.description, icon: achievement.icon, xp_reward: achievement.xp_reward, }, { onConflict: "id", ignoreDuplicates: true }, ); if (error && error.code !== "23505") { console.error(`Failed to upsert achievement ${achievement.id}:`, error); } else { seededAchievements[achievement.title] = uuidId; } } console.log("[Achievements] Seeded", Object.keys(seededAchievements).length, "achievements"); // Step 2: Try to find target user and award achievements (only if authorized) let targetUserId: string | null = null; let godModeAwarded = false; const awardedAchievementIds: string[] = []; if (canAwardToTarget && (targetEmail || targetUsername)) { let targetProfile: { id: string; username: string | null } | null = null; if (targetEmail) { // Look up by email via auth.admin, then fetch profile by user id const { data: authList } = await adminSupabase.auth.admin.listUsers(); const authUser = authList?.users?.find((u) => u.email === targetEmail); if (authUser) { const { data: prof } = await adminSupabase .from("user_profiles") .select("id, username") .eq("id", authUser.id) .single(); targetProfile = prof ?? null; } } else if (targetUsername) { const { data: prof } = await adminSupabase .from("user_profiles") .select("id, username") .eq("username", targetUsername) .single(); targetProfile = prof ?? null; } if (targetProfile?.id) { targetUserId = targetProfile.id; // Check if target user is an admin (for GOD Mode) const isTargetAdmin = targetEmail === "mrpiglr@gmail.com" || targetProfile.username === "mrpiglr"; // Award Welcome achievement to the user const welcomeId = seededAchievements["Welcome to AeThex"]; if (welcomeId) { const { error: welcomeError } = await adminSupabase .from("user_achievements") .upsert( { user_id: targetUserId, achievement_id: welcomeId }, { onConflict: "user_id,achievement_id" as any } ); if (!welcomeError || welcomeError.code === "23505") { awardedAchievementIds.push(welcomeId); } } // Award GOD Mode only to admin users if (isTargetAdmin) { const godModeId = seededAchievements["GOD Mode"]; if (godModeId) { const { error: godError } = await adminSupabase .from("user_achievements") .upsert( { user_id: targetUserId, achievement_id: godModeId }, { onConflict: "user_id,achievement_id" as any } ); if (!godError || godError.code === "23505") { godModeAwarded = true; awardedAchievementIds.push(godModeId); } } } } } return res.json({ ok: true, achievementsSeeded: Object.keys(seededAchievements).length, godModeAwarded, awardedAchievementIds, targetUserId, }); } catch (error: any) { console.error("activate achievements error", error); return res.status(500).json({ error: error?.message || "Failed to activate achievements", }); } }); // Blog endpoints (Supabase-backed) app.get("/api/blog", async (req, res) => { const limit = Math.max(1, Math.min(200, Number(req.query.limit) || 50)); const category = String(req.query.category || "").trim(); try { let query = adminSupabase .from("blog_posts") .select( "id, slug, title, excerpt, author, date, read_time, category, image, likes, comments, published_at, body_html", ) .order("published_at", { ascending: false, nullsLast: true } as any) .limit(limit); if (category) query = query.eq("category", category); const { data, error } = await query; if (error) { // If table doesn't exist, return empty array (client will use seed data) if ( error.message?.includes("does not exist") || error.code === "42P01" ) { console.log( "[Blog] blog_posts table not found, returning empty array", ); return res.json([]); } console.error("[Blog] Error fetching blog posts:", error); return res.status(500).json({ error: error.message }); } console.log( "[Blog] Successfully fetched", (data || []).length, "blog posts", ); res.json(data || []); } catch (e: any) { console.error("[Blog] Exception:", e); res.status(500).json({ error: e?.message || String(e) }); } }); app.get("/api/blog/:slug", async (req, res) => { const slug = String(req.params.slug || ""); if (!slug) return res.status(400).json({ error: "missing slug" }); try { const { data, error } = await adminSupabase .from("blog_posts") .select( "id, slug, title, excerpt, author, date, read_time, category, image, body_html, published_at", ) .eq("slug", slug) .single(); if (error) { if (error.code === "PGRST116") { // No rows returned - 404 return res.status(404).json({ error: "Blog post not found" }); } if ( error.message?.includes("does not exist") || error.code === "42P01" ) { // Table doesn't exist return res.status(404).json({ error: "Blog not configured" }); } return res.status(500).json({ error: error.message }); } res.json(data || null); } catch (e: any) { console.error("[Blog] Error fetching blog post:", e); res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/blog", async (req, res) => { const p = req.body || {}; const row = { slug: String(p.slug || "").trim(), title: String(p.title || "").trim(), excerpt: p.excerpt || null, author: p.author || null, date: p.date || null, read_time: p.read_time || null, category: p.category || null, image: p.image || null, likes: Number.isFinite(p.likes) ? p.likes : 0, comments: Number.isFinite(p.comments) ? p.comments : 0, body_html: p.body_html || null, published_at: p.published_at || new Date().toISOString(), } as any; if (!row.slug || !row.title) return res.status(400).json({ error: "slug and title required" }); try { const { data, error } = await adminSupabase .from("blog_posts") .upsert(row, { onConflict: "slug" as any }) .select() .single(); if (error) return res.status(500).json({ error: error.message }); res.json(data || row); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.delete("/api/blog/:slug", async (req, res) => { const slug = String(req.params.slug || ""); if (!slug) return res.status(400).json({ error: "missing slug" }); try { const { error } = await adminSupabase .from("blog_posts") .delete() .eq("slug", slug); if (error) return res.status(500).json({ error: error.message }); res.json({ ok: true }); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); // ===== DISCORD ACTIVITY API ENDPOINTS ===== // Activity Events app.get("/api/activity/events", async (req, res) => { try { const { data, error } = await adminSupabase .from("activity_events") .select("*, activity_event_rsvps(count)") .gte("start_time", new Date().toISOString()) .order("start_time", { ascending: true }) .limit(20); if (error) return res.status(500).json({ error: error.message }); res.json(data || []); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/activity/events", async (req, res) => { const { title, description, event_type, start_time, end_time, host_id, host_name, realm, max_attendees, image_url, external_url } = req.body || {}; if (!title || !start_time) return res.status(400).json({ error: "title and start_time required" }); try { const { data, error } = await adminSupabase .from("activity_events") .insert({ title, description, event_type, start_time, end_time, host_id, host_name, realm, max_attendees, image_url, external_url }) .select() .single(); if (error) return res.status(500).json({ error: error.message }); res.json(data); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/activity/events/:id/rsvp", async (req, res) => { const eventId = req.params.id; const { user_id, status } = req.body || {}; if (!user_id) return res.status(400).json({ error: "user_id required" }); try { const { data, error } = await adminSupabase .from("activity_event_rsvps") .upsert({ event_id: eventId, user_id, status: status || "going" }, { onConflict: "event_id,user_id" }) .select() .single(); if (error) return res.status(500).json({ error: error.message }); res.json(data); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.delete("/api/activity/events/:id/rsvp", async (req, res) => { const eventId = req.params.id; const user_id = req.query.user_id as string; if (!user_id) return res.status(400).json({ error: "user_id required" }); try { const { error } = await adminSupabase .from("activity_event_rsvps") .delete() .eq("event_id", eventId) .eq("user_id", user_id); if (error) return res.status(500).json({ error: error.message }); res.json({ ok: true }); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); // Activity Teams app.get("/api/activity/teams", async (req, res) => { try { const { data, error } = await adminSupabase .from("activity_teams") .select("*") .eq("is_active", true) .order("created_at", { ascending: false }) .limit(20); if (error) return res.status(500).json({ error: error.message }); res.json(data || []); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/activity/teams", async (req, res) => { const { title, description, owner_id, owner_name, realm, roles_needed, max_team, project_type } = req.body || {}; if (!title) return res.status(400).json({ error: "title required" }); try { const { data, error } = await adminSupabase .from("activity_teams") .insert({ title, description, owner_id, owner_name, realm, roles_needed, max_team, project_type }) .select() .single(); if (error) return res.status(500).json({ error: error.message }); res.json(data); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/activity/teams/:id/apply", async (req, res) => { const teamId = req.params.id; const { user_id, role_applied, message } = req.body || {}; if (!user_id) return res.status(400).json({ error: "user_id required" }); try { const { data, error } = await adminSupabase .from("activity_team_applications") .upsert({ team_id: teamId, user_id, role_applied, message }, { onConflict: "team_id,user_id" }) .select() .single(); if (error) return res.status(500).json({ error: error.message }); // Notify team owner const { data: team } = await adminSupabase .from("activity_teams") .select("owner_id, name") .eq("id", teamId) .single(); const { data: applicant } = await adminSupabase .from("user_profiles") .select("username, full_name") .eq("id", user_id) .single(); if (team?.owner_id && team.owner_id !== user_id) { const applicantName = (applicant as any)?.full_name || (applicant as any)?.username || "Someone"; await adminSupabase.from("notifications").insert({ user_id: team.owner_id, type: "info", title: `šŸ“‹ New team application`, message: `${applicantName} applied to join ${(team as any).name}${role_applied ? ` as ${role_applied}` : ""}`, }); } res.json(data); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); // Activity Projects app.get("/api/activity/projects", async (req, res) => { try { const { data, error } = await adminSupabase .from("activity_projects") .select("*") .order("upvotes", { ascending: false }) .limit(20); if (error) return res.status(500).json({ error: error.message }); res.json(data || []); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/activity/projects", async (req, res) => { const { title, description, owner_id, owner_name, realm, image_url, project_url, tags } = req.body || {}; if (!title) return res.status(400).json({ error: "title required" }); try { const { data, error } = await adminSupabase .from("activity_projects") .insert({ title, description, owner_id, owner_name, realm, image_url, project_url, tags }) .select() .single(); if (error) return res.status(500).json({ error: error.message }); res.json(data); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/activity/projects/:id/upvote", async (req, res) => { const projectId = req.params.id; const { user_id } = req.body || {}; if (!user_id) return res.status(400).json({ error: "user_id required" }); try { const { data: existing } = await adminSupabase .from("activity_project_upvotes") .select("id") .eq("project_id", projectId) .eq("user_id", user_id) .maybeSingle(); if (existing) return res.status(400).json({ error: "Already upvoted" }); const { error: insertError } = await adminSupabase .from("activity_project_upvotes") .insert({ project_id: projectId, user_id }); if (insertError) return res.status(500).json({ error: insertError.message }); await adminSupabase.rpc("increment_project_upvotes", { project_id: projectId }).catch(() => { adminSupabase.from("activity_projects").update({ upvotes: adminSupabase.rpc("get_upvote_count", { pid: projectId }) }).eq("id", projectId); }); // Notify project owner (not self-upvotes, and throttle: only on milestone counts) const { data: project } = await adminSupabase .from("activity_projects") .select("owner_id, title, upvotes") .eq("id", projectId) .single(); const upvotes = (project as any)?.upvotes || 0; const milestones = [5, 10, 25, 50, 100]; if (project && (project as any).owner_id !== user_id && milestones.includes(upvotes)) { await adminSupabase.from("notifications").insert({ user_id: (project as any).owner_id, type: "success", title: `šŸš€ ${upvotes} upvotes on your project`, message: `"${(project as any).title}" just hit ${upvotes} upvotes!`, }); } res.json({ ok: true }); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); // Activity Polls app.get("/api/activity/polls", async (req, res) => { try { const { data, error } = await adminSupabase .from("activity_polls") .select("*") .eq("is_active", true) .order("created_at", { ascending: false }) .limit(10); if (error) return res.status(500).json({ error: error.message }); const pollsWithVotes = await Promise.all((data || []).map(async (poll: any) => { const { data: votes } = await adminSupabase .from("activity_poll_votes") .select("option_index") .eq("poll_id", poll.id); const voteCounts: Record = {}; (votes || []).forEach((v: any) => { voteCounts[v.option_index] = (voteCounts[v.option_index] || 0) + 1; }); return { ...poll, vote_counts: voteCounts, total_votes: votes?.length || 0 }; })); res.json(pollsWithVotes); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/activity/polls", async (req, res) => { const { question, options, creator_id, creator_name, expires_at } = req.body || {}; if (!question || !options || !Array.isArray(options)) return res.status(400).json({ error: "question and options array required" }); try { const { data, error } = await adminSupabase .from("activity_polls") .insert({ question, options, creator_id, creator_name, expires_at }) .select() .single(); if (error) return res.status(500).json({ error: error.message }); res.json(data); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/activity/polls/:id/vote", async (req, res) => { const pollId = req.params.id; const { user_id, option_index } = req.body || {}; if (!user_id || option_index === undefined) return res.status(400).json({ error: "user_id and option_index required" }); try { const { data, error } = await adminSupabase .from("activity_poll_votes") .upsert({ poll_id: pollId, user_id, option_index }, { onConflict: "poll_id,user_id" }) .select() .single(); if (error) return res.status(500).json({ error: error.message }); res.json(data); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); // Activity Chat app.get("/api/activity/chat", async (req, res) => { const limit = Math.min(100, Number(req.query.limit) || 50); try { const { data, error } = await adminSupabase .from("activity_chat_messages") .select("*") .order("created_at", { ascending: false }) .limit(limit); if (error) return res.status(500).json({ error: error.message }); res.json((data || []).reverse()); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/activity/chat", async (req, res) => { const { user_id, username, avatar_url, content } = req.body || {}; if (!user_id || !content) return res.status(400).json({ error: "user_id and content required" }); try { const { data, error } = await adminSupabase .from("activity_chat_messages") .insert({ user_id, username, avatar_url, content }) .select() .single(); if (error) return res.status(500).json({ error: error.message }); res.json(data); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); // Activity Challenges app.get("/api/activity/challenges", async (req, res) => { try { const { data, error } = await adminSupabase .from("activity_challenges") .select("*") .eq("is_active", true) .order("ends_at", { ascending: true }); if (error) return res.status(500).json({ error: error.message }); res.json(data || []); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/activity/challenges", async (req, res) => { const { title, description, xp_reward, challenge_type, target_count, starts_at, ends_at } = req.body || {}; if (!title) return res.status(400).json({ error: "title required" }); try { const { data, error } = await adminSupabase .from("activity_challenges") .insert({ title, description, xp_reward, challenge_type, target_count, starts_at, ends_at }) .select() .single(); if (error) return res.status(500).json({ error: error.message }); res.json(data); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.get("/api/activity/challenges/:id/progress", async (req, res) => { const challengeId = req.params.id; const user_id = req.query.user_id as string; if (!user_id) return res.status(400).json({ error: "user_id required" }); try { const { data, error } = await adminSupabase .from("activity_challenge_progress") .select("*") .eq("challenge_id", challengeId) .eq("user_id", user_id) .maybeSingle(); if (error) return res.status(500).json({ error: error.message }); res.json(data || { progress: 0, completed: false }); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/activity/challenges/:id/progress", async (req, res) => { const challengeId = req.params.id; const { user_id, progress, completed } = req.body || {}; if (!user_id) return res.status(400).json({ error: "user_id required" }); try { const { data, error } = await adminSupabase .from("activity_challenge_progress") .upsert({ challenge_id: challengeId, user_id, progress: progress || 0, completed: completed || false, completed_at: completed ? new Date().toISOString() : null }, { onConflict: "challenge_id,user_id" }) .select() .single(); if (error) return res.status(500).json({ error: error.message }); res.json(data); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); // Creator Spotlight app.get("/api/activity/spotlight", async (req, res) => { try { const { data, error } = await adminSupabase .from("activity_spotlight") .select("*") .order("votes", { ascending: false }) .limit(10); if (error) return res.status(500).json({ error: error.message }); res.json(data || []); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/activity/spotlight/:id/vote", async (req, res) => { const spotlightId = req.params.id; const { voter_id } = req.body || {}; if (!voter_id) return res.status(400).json({ error: "voter_id required" }); try { const { data: existing } = await adminSupabase .from("activity_spotlight_votes") .select("id") .eq("spotlight_id", spotlightId) .eq("voter_id", voter_id) .maybeSingle(); if (existing) return res.status(400).json({ error: "Already voted" }); const { error: insertError } = await adminSupabase .from("activity_spotlight_votes") .insert({ spotlight_id: spotlightId, voter_id }); if (insertError) return res.status(500).json({ error: insertError.message }); await adminSupabase .from("activity_spotlight") .update({ votes: adminSupabase.rpc("get_spotlight_votes", { sid: spotlightId }) }) .eq("id", spotlightId) .catch(() => {}); res.json({ ok: true }); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); // Leaderboard (real XP data) app.get("/api/activity/leaderboard", async (req, res) => { const limit = Math.min(50, Number(req.query.limit) || 20); try { const { data, error } = await adminSupabase .from("user_profiles") .select("id, username, full_name, avatar_url, total_xp, level, current_streak") .order("total_xp", { ascending: false }) .limit(limit); if (error) return res.status(500).json({ error: error.message }); const ranked = (data || []).map((user: any, index: number) => ({ rank: index + 1, user_id: user.id, username: user.username || user.full_name || "Anonymous", avatar_url: user.avatar_url, total_xp: user.total_xp || 0, level: user.level || 1, current_streak: user.current_streak || 0 })); res.json(ranked); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); // User Badges (real data) app.get("/api/activity/badges/:userId", async (req, res) => { const userId = req.params.userId; try { const { data, error } = await adminSupabase .from("user_badges") .select("*, badges(*)") .eq("user_id", userId); if (error) { if (error.code === "42P01" || error.message?.includes("does not exist")) { return res.json([]); } return res.status(500).json({ error: error.message }); } res.json(data || []); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); // User Stats for Activity app.get("/api/activity/user-stats/:userId", async (req, res) => { const userId = req.params.userId; try { const { data, error } = await adminSupabase .from("user_profiles") .select("total_xp, level, current_streak, longest_streak") .eq("id", userId) .maybeSingle(); if (error) return res.status(500).json({ error: error.message }); if (!data) return res.json({ total_xp: 0, level: 1, current_streak: 0, longest_streak: 0 }); const { count } = await adminSupabase .from("user_profiles") .select("*", { count: "exact", head: true }) .gt("total_xp", data.total_xp || 0); res.json({ ...data, rank: (count || 0) + 1 }); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); // ===== END DISCORD ACTIVITY API ENDPOINTS ===== app.get("/api/applications-old", async (req, res) => { const owner = String(req.query.owner || ""); if (!owner) return res.status(400).json({ error: "owner required" }); try { const { data, error } = await adminSupabase .from("aethex_applications") .select(`*`) .eq("creator_id", owner) .order("applied_at", { ascending: false }) .limit(50); if (error) return res.status(500).json({ error: error.message }); res.json(data || []); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.get("/api/opportunities/applications", async (req, res) => { const requester = String(req.query.email || "").toLowerCase(); if (!requester || requester !== ownerEmail) { return res.status(403).json({ error: "forbidden" }); } try { const { data, error } = await adminSupabase .from("applications") .select("*") .order("submitted_at", { ascending: false }) .limit(200); if (error) { if (isTableMissing(error)) { return res.json([]); } return res.status(500).json({ error: error.message }); } return res.json(data || []); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // Invites API const baseUrl = process.env.PUBLIC_BASE_URL || process.env.SITE_URL || "https://aethex.biz"; const safeEmail = (v?: string | null) => (v || "").trim().toLowerCase(); const accrue = async ( userId: string, kind: "xp" | "loyalty" | "reputation", amount: number, type: string, meta?: any, ) => { const amt = Math.max(0, Math.floor(amount)); try { await adminSupabase.from("reward_events").insert({ user_id: userId, type, points_kind: kind, amount: amt, metadata: meta || null, }); } catch {} const col = kind === "xp" ? "total_xp" : kind === "loyalty" ? "loyalty_points" : "reputation_score"; const { data: row } = await adminSupabase .from("user_profiles") .select(`id, ${col}, level`) .eq("id", userId) .maybeSingle(); const current = Number((row as any)?.[col] || 0); const updates: any = { [col]: current + amt }; if (col === "total_xp") { const total = current + amt; updates.level = Math.max(1, Math.floor(total / 1000) + 1); } await adminSupabase .from("user_profiles") .update(updates) .eq("id", userId); }; app.post("/api/invites", async (req, res) => { const { inviter_id, invitee_email, message } = (req.body || {}) as { inviter_id?: string; invitee_email?: string; message?: string | null; }; if (!inviter_id || !invitee_email) { return res .status(400) .json({ error: "inviter_id and invitee_email are required" }); } const email = safeEmail(invitee_email); const token = randomUUID(); try { const { data: inviterProfile } = await adminSupabase .from("user_profiles") .select("full_name, username") .eq("id", inviter_id) .maybeSingle(); const inviterName = (inviterProfile as any)?.full_name || (inviterProfile as any)?.username || "An AeThex member"; const { data, error } = await adminSupabase .from("invites") .insert({ inviter_id, invitee_email: email, token, message: message || null, status: "pending", }) .select() .single(); if (error) return res.status(500).json({ error: error.message }); const inviteUrl = `${baseUrl}/login?invite=${encodeURIComponent(token)}`; if (emailService.isConfigured) { try { await emailService.sendInviteEmail({ to: email, inviteUrl, inviterName, message: message || null, }); } catch (e: any) { console.warn("Failed to send invite email", e?.message || e); } } await accrue(inviter_id, "loyalty", 5, "invite_sent", { invitee: email, }); try { await adminSupabase.from("notifications").insert({ user_id: inviter_id, type: "info", title: "Invite sent", message: `Invitation sent to ${email}`, }); } catch {} return res.json({ ok: true, invite: data, inviteUrl, token }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); app.get("/api/invites", async (req, res) => { const inviter = String(req.query.inviter_id || ""); if (!inviter) return res.status(400).json({ error: "inviter_id required" }); try { const { data, error } = await adminSupabase .from("invites") .select("*") .eq("inviter_id", inviter) .order("created_at", { ascending: false }); if (error) return res.status(500).json({ error: error.message }); return res.json(data || []); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/invites/accept", async (req, res) => { const { token, acceptor_id } = (req.body || {}) as { token?: string; acceptor_id?: string; }; if (!token || !acceptor_id) { return res .status(400) .json({ error: "token and acceptor_id required" }); } try { const { data: invite, error } = await adminSupabase .from("invites") .select("*") .eq("token", token) .eq("status", "pending") .maybeSingle(); if (error) return res.status(500).json({ error: error.message }); if (!invite) return res.status(404).json({ error: "invalid_invite" }); const now = new Date().toISOString(); const { error: upErr } = await adminSupabase .from("invites") .update({ status: "accepted", accepted_by: acceptor_id, accepted_at: now, }) .eq("id", (invite as any).id); if (upErr) return res.status(500).json({ error: upErr.message }); const inviterId = (invite as any).inviter_id as string; if (inviterId && inviterId !== acceptor_id) { await adminSupabase .from("user_connections") .upsert({ user_id: inviterId, connection_id: acceptor_id } as any) .catch(() => undefined); await adminSupabase .from("user_connections") .upsert({ user_id: acceptor_id, connection_id: inviterId } as any) .catch(() => undefined); } if (inviterId) { await accrue(inviterId, "xp", 100, "invite_accepted", { token }); await accrue(inviterId, "loyalty", 50, "invite_accepted", { token }); await accrue(inviterId, "reputation", 2, "invite_accepted", { token, }); try { await adminSupabase.from("notifications").insert({ user_id: inviterId, type: "success", title: "Invite accepted", message: "Your invitation was accepted. You're now connected.", }); } catch {} } await accrue(acceptor_id, "xp", 50, "invite_accepted", { token }); await accrue(acceptor_id, "reputation", 1, "invite_accepted", { token, }); try { await adminSupabase.from("notifications").insert({ user_id: acceptor_id, type: "success", title: "Connected", message: "Connection established via invitation.", }); } catch {} return res.json({ ok: true }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // Follow/unfollow with notifications app.post("/api/social/follow", async (req, res) => { const { follower_id, following_id } = (req.body || {}) as { follower_id?: string; following_id?: string; }; if (!follower_id || !following_id) return res .status(400) .json({ error: "follower_id and following_id required" }); try { await adminSupabase .from("user_follows") .upsert({ follower_id, following_id } as any, { onConflict: "follower_id,following_id" as any, }); await accrue(follower_id, "loyalty", 5, "follow_user", { following_id, }); const { data: follower } = await adminSupabase .from("user_profiles") .select("full_name, username") .eq("id", follower_id) .maybeSingle(); const followerName = (follower as any)?.full_name || (follower as any)?.username || "Someone"; await adminSupabase.from("notifications").insert({ user_id: following_id, type: "info", title: "New follower", message: `${followerName} started following you`, }); return res.json({ ok: true }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/social/unfollow", async (req, res) => { const { follower_id, following_id } = (req.body || {}) as { follower_id?: string; following_id?: string; }; if (!follower_id || !following_id) return res .status(400) .json({ error: "follower_id and following_id required" }); try { await adminSupabase .from("user_follows") .delete() .eq("follower_id", follower_id) .eq("following_id", following_id); return res.json({ ok: true }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // Get following list app.get("/api/social/following", async (req, res) => { const userId = req.query.userId as string; if (!userId) { return res .status(400) .json({ error: "userId query parameter required" }); } try { const { data, error } = await adminSupabase .from("user_follows") .select("following_id") .eq("follower_id", userId); if (error) { return res .status(500) .json({ error: "Failed to fetch following list" }); } return res.json({ data: (data || []).map((r: any) => r.following_id), }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // Get followers list app.get("/api/social/followers", async (req, res) => { const userId = req.query.userId as string; if (!userId) { return res .status(400) .json({ error: "userId query parameter required" }); } try { const { data, error } = await adminSupabase .from("user_follows") .select("follower_id") .eq("following_id", userId); if (error) { return res .status(500) .json({ error: "Failed to fetch followers list" }); } return res.json({ data: (data || []).map((r: any) => r.follower_id), }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // Community post likes app.post("/api/community/posts/:id/like", async (req, res) => { const postId = req.params.id; const { user_id } = (req.body || {}) as { user_id?: string }; if (!postId || !user_id) return res.status(400).json({ error: "post id and user_id required" }); try { // Get post author info before liking const { data: post } = await adminSupabase .from("community_posts") .select("user_id") .eq("id", postId) .single(); const { error: likeErr } = await adminSupabase .from("community_post_likes") .upsert({ post_id: postId, user_id } as any, { onConflict: "post_id,user_id" as any, }); if (likeErr) return res.status(500).json({ error: likeErr.message }); const { data: c } = await adminSupabase .from("community_post_likes") .select("post_id", { count: "exact", head: true }) .eq("post_id", postId); const count = (c as any)?.length ? (c as any).length : (c as any)?.count || null; if (typeof count === "number") { await adminSupabase .from("community_posts") .update({ likes_count: count }) .eq("id", postId); } // Notify post author of like (only if different user) if (post?.user_id && post.user_id !== user_id) { try { const { data: liker } = await adminSupabase .from("user_profiles") .select("full_name, username") .eq("id", user_id) .single(); const likerName = (liker as any)?.full_name || (liker as any)?.username || "Someone"; await adminSupabase.from("notifications").insert({ user_id: post.user_id, type: "info", title: "ā¤ļø Your post was liked", message: `${likerName} liked your post.`, }); } catch (notifError) { console.warn("Failed to create like notification:", notifError); } } return res.json({ ok: true, likes: typeof count === "number" ? count : undefined, }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/community/posts/:id/unlike", async (req, res) => { const postId = req.params.id; const { user_id } = (req.body || {}) as { user_id?: string }; if (!postId || !user_id) return res.status(400).json({ error: "post id and user_id required" }); try { await adminSupabase .from("community_post_likes") .delete() .eq("post_id", postId) .eq("user_id", user_id); const { data: c } = await adminSupabase .from("community_post_likes") .select("post_id", { count: "exact", head: true }) .eq("post_id", postId); const count = (c as any)?.length ? (c as any).length : (c as any)?.count || null; if (typeof count === "number") { await adminSupabase .from("community_posts") .update({ likes_count: count }) .eq("id", postId); } return res.json({ ok: true, likes: typeof count === "number" ? count : undefined, }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // Community post comments app.get("/api/community/posts/:id/comments", async (req, res) => { const postId = req.params.id; try { const { data, error } = await adminSupabase .from("community_comments") .select( "*, user_profiles:user_id ( id, full_name, username, avatar_url )", ) .eq("post_id", postId) .order("created_at", { ascending: true }); if (error) return res.status(500).json({ error: error.message }); res.json(data || []); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/community/posts/:id/comments", async (req, res) => { const postId = req.params.id; const { user_id, content } = (req.body || {}) as { user_id?: string; content?: string; }; if (!user_id || !content) return res.status(400).json({ error: "user_id and content required" }); try { // Get post author info const { data: post } = await adminSupabase .from("community_posts") .select("user_id") .eq("id", postId) .single(); const { data, error } = await adminSupabase .from("community_comments") .insert({ post_id: postId, user_id, content } as any) .select() .single(); if (error) return res.status(500).json({ error: error.message }); const { data: agg } = await adminSupabase .from("community_comments") .select("post_id", { count: "exact", head: true }) .eq("post_id", postId); const count = (agg as any)?.count || null; if (typeof count === "number") { await adminSupabase .from("community_posts") .update({ comments_count: count }) .eq("id", postId); } // Notify post author of comment (only if different user) if (post?.user_id && post.user_id !== user_id) { try { const { data: commenter } = await adminSupabase .from("user_profiles") .select("full_name, username") .eq("id", user_id) .single(); const commenterName = (commenter as any)?.full_name || (commenter as any)?.username || "Someone"; const preview = content.substring(0, 50) + (content.length > 50 ? "..." : ""); await adminSupabase.from("notifications").insert({ user_id: post.user_id, type: "info", title: "šŸ’¬ New comment on your post", message: `${commenterName} commented: "${preview}"`, }); } catch (notifError) { console.warn("Failed to create comment notification:", notifError); } } res.json(data); } catch (e: any) { res.status(500).json({ error: e?.message || String(e) }); } }); // Endorse with notification app.post("/api/social/endorse", async (req, res) => { const { endorser_id, endorsed_id, skill } = (req.body || {}) as { endorser_id?: string; endorsed_id?: string; skill?: string; }; if (!endorser_id || !endorsed_id || !skill) return res .status(400) .json({ error: "endorser_id, endorsed_id, skill required" }); try { await adminSupabase .from("endorsements") .insert({ endorser_id, endorsed_id, skill } as any); await accrue(endorsed_id, "reputation", 2, "endorsement_received", { skill, from: endorser_id, }); const { data: endorser } = await adminSupabase .from("user_profiles") .select("full_name, username") .eq("id", endorser_id) .maybeSingle(); const endorserName = (endorser as any)?.full_name || (endorser as any)?.username || "Someone"; await adminSupabase.from("notifications").insert({ user_id: endorsed_id, type: "success", title: "New endorsement", message: `${endorserName} endorsed you for ${skill}`, }); return res.json({ ok: true }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // Activity bus: publish event and fanout notifications app.post("/api/activity/publish", async (req, res) => { const { actor_id, verb, object_type, object_id, target_user_ids, target_team_id, target_project_id, metadata, } = (req.body || {}) as any; if (!actor_id || !verb || !object_type) { return res .status(400) .json({ error: "actor_id, verb, object_type required" }); } try { const { data: eventRow, error: evErr } = await adminSupabase .from("activity_events") .insert({ actor_id, verb, object_type, object_id: object_id || null, target_id: target_team_id || target_project_id || null, metadata: metadata || null, } as any) .select() .single(); if (evErr) return res.status(500).json({ error: evErr.message }); const notify = async ( userId: string, title: string, message?: string, ) => { await adminSupabase.from("notifications").insert({ user_id: userId, type: "info", title, message: message || null, }); }; // Notify explicit targets if (Array.isArray(target_user_ids) && target_user_ids.length) { for (const uid of target_user_ids) { await notify( uid, `${verb} Ā· ${object_type}`, (metadata && metadata.summary) || null, ); } } // Notify team members if provided if (target_team_id) { const { data: members } = await adminSupabase .from("team_memberships") .select("user_id") .eq("team_id", target_team_id); for (const m of members || []) { await notify( (m as any).user_id, `${verb} Ā· ${object_type}`, (metadata && metadata.summary) || null, ); } } // Notify project members if provided if (target_project_id) { const { data: members } = await adminSupabase .from("project_members") .select("user_id") .eq("project_id", target_project_id); for (const m of members || []) { await notify( (m as any).user_id, `${verb} Ā· ${object_type}`, (metadata && metadata.summary) || null, ); } } return res.json({ ok: true, event: eventRow }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/rewards/apply", async (req, res) => { const { user_id, action, amount } = (req.body || {}) as { user_id?: string; action?: string; amount?: number | null; }; if (!user_id || !action) { return res.status(400).json({ error: "user_id and action required" }); } try { const actionKey = String(action); switch (actionKey) { case "post_created": await accrue(user_id, "xp", amount ?? 25, actionKey); await accrue(user_id, "loyalty", 5, actionKey); break; case "follow_user": await accrue(user_id, "loyalty", 5, actionKey); break; case "endorsement_received": await accrue(user_id, "reputation", amount ?? 2, actionKey); break; case "daily_login": await accrue(user_id, "xp", amount ?? 10, actionKey); await accrue(user_id, "loyalty", 2, actionKey); break; default: await accrue(user_id, "xp", Math.max(0, amount ?? 0), actionKey); break; } return res.json({ ok: true }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // Investors: capture interest app.post("/api/investors/interest", async (req, res) => { const { name, email, amount, accredited, message } = (req.body || {}) as { name?: string; email?: string; amount?: string; accredited?: boolean; message?: string; }; if (!email) return res.status(400).json({ error: "email required" }); try { const subject = `Investor interest: ${name || email}`; const body = [ `Name: ${name || "N/A"}`, `Email: ${email}`, `Amount: ${amount || "N/A"}`, `Accredited: ${accredited ? "Yes" : "No / Unknown"}`, message ? `\nMessage:\n${message}` : "", ].join("\n"); try { const { emailService } = await import("./email"); if (emailService.isConfigured) { await (emailService as any).sendInviteEmail({ to: process.env.VERIFY_SUPPORT_EMAIL || "support@aethex.biz", inviteUrl: "https://aethex.dev/investors", inviterName: name || email, message: body, }); } } catch (e) { /* ignore email errors to not block */ } return res.json({ ok: true }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // Leads: capture website leads (Wix microsite and others) app.post("/api/leads", async (req, res) => { const { name, email, company, website, budget, timeline, message, source, } = (req.body || {}) as { name?: string; email?: string; company?: string; website?: string; budget?: string; timeline?: string; message?: string; source?: string; }; if (!email) return res.status(400).json({ error: "email required" }); try { const lines = [ `Source: ${source || "web"}`, `Name: ${name || "N/A"}`, `Email: ${email}`, `Company: ${company || "N/A"}`, `Website: ${website || "N/A"}`, `Budget: ${budget || "N/A"}`, `Timeline: ${timeline || "N/A"}`, message ? `\nMessage:\n${message}` : "", ].join("\n"); try { if (emailService.isConfigured) { const base = process.env.PUBLIC_BASE_URL || process.env.SITE_URL || "https://aethex.dev"; await (emailService as any).sendInviteEmail({ to: process.env.VERIFY_SUPPORT_EMAIL || "support@aethex.biz", inviteUrl: `${base}/wix`, inviterName: name || email, message: lines, }); } } catch (e) { // continue even if email fails } return res.json({ ok: true }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // Roblox inbound callback (from HttpService or external automation) app.get("/roblox-callback", (_req, res) => res.json({ ok: true })); app.post("/roblox-callback", async (req, res) => { const shared = process.env.ROBLOX_SHARED_SECRET || process.env.ROBLOX_WEBHOOK_SECRET || ""; const sig = String( req.get("x-shared-secret") || req.get("x-roblox-signature") || "", ); if (shared && sig !== shared) return res.status(401).json({ error: "unauthorized" }); try { const payload = { ...((req.body as any) || {}), ip: (req.headers["x-forwarded-for"] as string) || req.ip, ua: req.get("user-agent") || null, received_at: new Date().toISOString(), }; // Best-effort persist if table exists try { await adminSupabase.from("roblox_events").insert({ event_type: (payload as any).event || null, payload, } as any); } catch (e: any) { // ignore if table missing or RLS blocks } return res.json({ ok: true }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // Staff: users search/listing app.get("/api/staff/users", async (req, res) => { const limit = Math.max(1, Math.min(50, Number(req.query.limit) || 20)); const q = String(req.query.q || "") .trim() .toLowerCase(); try { const { data, error } = await adminSupabase .from("user_profiles") .select( "id, username, full_name, avatar_url, user_type, created_at, updated_at", ) .order("created_at", { ascending: false }) .limit(limit); if (error) return res.status(500).json({ error: error.message }); let rows = (data || []) as any[]; if (q) { rows = rows.filter((r) => { const name = String(r.full_name || r.username || "").toLowerCase(); return name.includes(q); }); } return res.json(rows); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // Mentorship API app.get("/api/mentors", async (req, res) => { const limit = Math.max(1, Math.min(50, Number(req.query.limit) || 20)); const available = String(req.query.available || "true").toLowerCase() !== "false"; const expertise = String(req.query.expertise || "").trim(); const q = String(req.query.q || "") .trim() .toLowerCase(); try { const { data, error } = await adminSupabase .from("mentors") .select( `user_id, bio, expertise, available, hourly_rate, created_at, updated_at, user_profiles:user_id ( id, username, full_name, avatar_url, bio )`, ) .eq("available", available) .order("updated_at", { ascending: false }) .limit(limit); if (error) { if (isTableMissing(error)) return res.json([]); return res.status(500).json({ error: error.message }); } let rows = (data || []) as any[]; if (expertise) { const terms = expertise .split(",") .map((s) => s.trim().toLowerCase()) .filter(Boolean); if (terms.length) { rows = rows.filter( (r: any) => Array.isArray(r.expertise) && r.expertise.some((e: string) => terms.includes(String(e).toLowerCase()), ), ); } } if (q) { rows = rows.filter((r: any) => { const up = (r as any).user_profiles || {}; const name = String( up.full_name || up.username || "", ).toLowerCase(); const bio = String(r.bio || up.bio || "").toLowerCase(); return name.includes(q) || bio.includes(q); }); } return res.json(rows); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // System Status app.get("/api/status", async (req, res) => { const startedAt = Date.now(); const host = `${req.protocol}://${req.get("host")}`; const time = async (fn: () => Promise) => { const t0 = Date.now(); try { await fn(); return { ok: true, ms: Date.now() - t0 }; } catch (e) { return { ok: false, ms: Date.now() - t0, error: (e as any)?.message || String(e), }; } }; // Database check (user_profiles) const dbCheck = await time(async () => { await adminSupabase .from("user_profiles") .select("id", { head: true, count: "exact" }) .limit(1); }); // API/Core check (community_posts) const apiCheck = await time(async () => { await adminSupabase .from("community_posts") .select("id", { head: true, count: "exact" }) .limit(1); }); // Auth check const authCheck = await time(async () => { const admin = (adminSupabase as any)?.auth?.admin; if (!admin) throw new Error("auth admin unavailable"); await admin.listUsers({ page: 1, perPage: 1 } as any); }); // CDN/static const cdnCheck = await time(async () => { const resp = await fetch(`${host}/robots.txt`).catch(() => null); if (!resp || !resp.ok) throw new Error("robots not reachable"); }); const statusFrom = (c: { ok: boolean; ms: number }) => !c.ok ? "outage" : c.ms > 800 ? "degraded" : "operational"; const nowIso = new Date().toISOString(); const services = [ { name: "AeThex Core API", status: statusFrom(apiCheck) as any, responseTime: apiCheck.ms, uptime: apiCheck.ok ? "99.99%" : "--", lastCheck: nowIso, description: "Main application API and endpoints", }, { name: "Database Services", status: statusFrom(dbCheck) as any, responseTime: dbCheck.ms, uptime: dbCheck.ok ? "99.99%" : "--", lastCheck: nowIso, description: "Supabase Postgres and Storage", }, { name: "CDN & Assets", status: statusFrom(cdnCheck) as any, responseTime: cdnCheck.ms, uptime: cdnCheck.ok ? "99.95%" : "--", lastCheck: nowIso, description: "Static and media delivery", }, { name: "Authentication", status: statusFrom(authCheck) as any, responseTime: authCheck.ms, uptime: authCheck.ok ? "99.97%" : "--", lastCheck: nowIso, description: "OAuth and email auth (Supabase)", }, ]; const avgRt = Math.round( services.reduce((a, s) => a + (Number(s.responseTime) || 0), 0) / services.length, ); const errCount = services.filter((s) => s.status === "outage").length; const warnCount = services.filter((s) => s.status === "degraded").length; // Active users (best effort) let activeUsers = "--"; try { const { count } = await adminSupabase .from("user_profiles") .select("id", { head: true, count: "exact" }); if (typeof count === "number") activeUsers = count.toLocaleString(); } catch {} const metrics = [ { name: "Global Uptime", value: errCount ? "99.5" : warnCount ? "99.9" : "99.99", unit: "%", status: errCount ? "critical" : warnCount ? "warning" : "good", icon: "Activity", }, { name: "Response Time", value: String(avgRt), unit: "ms", status: avgRt > 800 ? "critical" : avgRt > 400 ? "warning" : "good", icon: "Zap", }, { name: "Active Users", value: activeUsers, unit: "", status: "good", icon: "Globe", }, { name: "Error Rate", value: String(errCount), unit: " outages", status: errCount ? "critical" : warnCount ? "warning" : "good", icon: "Shield", }, ]; res.json({ updatedAt: new Date().toISOString(), durationMs: Date.now() - startedAt, services, metrics, incidents: [], }); }); app.post("/api/mentors/apply", async (req, res) => { const { user_id, bio, expertise, hourly_rate, available } = (req.body || {}) as { user_id?: string; bio?: string | null; expertise?: string[]; hourly_rate?: number | null; available?: boolean | null; }; if (!user_id) return res.status(400).json({ error: "user_id required" }); try { const payload: any = { user_id, bio: bio ?? null, expertise: Array.isArray(expertise) ? expertise : [], available: available ?? true, hourly_rate: typeof hourly_rate === "number" ? hourly_rate : null, }; const { data, error } = await adminSupabase .from("mentors") .upsert(payload, { onConflict: "user_id" as any }) .select() .single(); if (error) return res.status(500).json({ error: error.message }); try { await adminSupabase.from("notifications").insert({ user_id, type: "success", title: "Mentor profile updated", message: "Your mentor availability and expertise are saved.", }); } catch {} return res.json(data || payload); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/mentorship/request", async (req, res) => { const { mentee_id, mentor_id, message } = (req.body || {}) as { mentee_id?: string; mentor_id?: string; message?: string | null; }; if (!mentee_id || !mentor_id) { return res .status(400) .json({ error: "mentee_id and mentor_id required" }); } if (mentee_id === mentor_id) { return res.status(400).json({ error: "cannot request yourself" }); } try { const { data, error } = await adminSupabase .from("mentorship_requests") .insert({ mentee_id, mentor_id, message: message || null } as any) .select() .single(); if (error) return res.status(500).json({ error: error.message }); try { const { data: mentee } = await adminSupabase .from("user_profiles") .select("full_name, username") .eq("id", mentee_id) .maybeSingle(); const menteeName = (mentee as any)?.full_name || (mentee as any)?.username || "Someone"; await adminSupabase.from("notifications").insert({ user_id: mentor_id, type: "info", title: "Mentorship request", message: `${menteeName} requested mentorship.`, }); } catch {} return res.json({ ok: true, request: data }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/mentorship/requests/:id/status", async (req, res) => { const id = String(req.params.id || ""); const { actor_id, status } = (req.body || {}) as { actor_id?: string; status?: string; }; if (!id || !actor_id || !status) { return res.status(400).json({ error: "id, actor_id, status required" }); } const allowed = ["accepted", "rejected", "cancelled"]; if (!allowed.includes(String(status))) { return res.status(400).json({ error: "invalid status" }); } try { const { data: reqRow, error } = await adminSupabase .from("mentorship_requests") .select("id, mentor_id, mentee_id, status") .eq("id", id) .maybeSingle(); if (error) return res.status(500).json({ error: error.message }); if (!reqRow) return res.status(404).json({ error: "not_found" }); const isMentor = (reqRow as any).mentor_id === actor_id; const isMentee = (reqRow as any).mentee_id === actor_id; if ((status === "accepted" || status === "rejected") && !isMentor) { return res.status(403).json({ error: "forbidden" }); } if (status === "cancelled" && !isMentee) { return res.status(403).json({ error: "forbidden" }); } const { data, error: upErr } = await adminSupabase .from("mentorship_requests") .update({ status }) .eq("id", id) .select() .single(); if (upErr) return res.status(500).json({ error: upErr.message }); try { const target = status === "cancelled" ? (reqRow as any).mentor_id : (reqRow as any).mentee_id; const title = status === "accepted" ? "Mentorship accepted" : status === "rejected" ? "Mentorship rejected" : "Mentorship cancelled"; await adminSupabase.from("notifications").insert({ user_id: target, type: "info", title, message: null, }); } catch {} return res.json({ ok: true, request: data }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); app.get("/api/mentorship/requests", async (req, res) => { const userId = String(req.query.user_id || ""); const role = String(req.query.role || "").toLowerCase(); if (!userId) return res.status(400).json({ error: "user_id required" }); try { let query = adminSupabase .from("mentorship_requests") .select( `*, mentor:user_profiles!mentorship_requests_mentor_id_fkey ( id, full_name, username, avatar_url ), mentee:user_profiles!mentorship_requests_mentee_id_fkey ( id, full_name, username, avatar_url )`, ) .order("created_at", { ascending: false }); if (role === "mentor") query = query.eq("mentor_id", userId); else if (role === "mentee") query = query.eq("mentee_id", userId); else query = query.or(`mentor_id.eq.${userId},mentee_id.eq.${userId}`); const { data, error } = await query; if (error) { if (isTableMissing(error)) return res.json([]); return res.status(500).json({ error: error.message }); } return res.json(data || []); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // Staff: list all mentorship requests (limited) app.get("/api/mentorship/requests/all", async (req, res) => { const limit = Math.max(1, Math.min(200, Number(req.query.limit) || 50)); const status = String(req.query.status || "").toLowerCase(); try { let query = adminSupabase .from("mentorship_requests") .select( `*, mentor:user_profiles!mentorship_requests_mentor_id_fkey ( id, full_name, username, avatar_url ), mentee:user_profiles!mentorship_requests_mentee_id_fkey ( id, full_name, username, avatar_url )`, ) .order("created_at", { ascending: false }) .limit(limit); if (status) query = query.eq("status", status); const { data, error } = await query; if (error) return res.status(500).json({ error: error.message }); return res.json(data || []); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // Moderation API app.post("/api/moderation/reports", async (req, res) => { const { reporter_id, target_type, target_id, reason, details } = (req.body || {}) as any; if (!target_type || !reason) { return res .status(400) .json({ error: "target_type and reason required" }); } try { const { data, error } = await adminSupabase .from("moderation_reports") .insert({ reporter_id: reporter_id || null, target_type, target_id: target_id || null, reason, details: details || null, } as any) .select() .single(); if (error) return res.status(500).json({ error: error.message }); return res.json(data); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); app.get("/api/moderation/reports", async (req, res) => { const status = String(req.query.status || "open").toLowerCase(); const limit = Math.max(1, Math.min(200, Number(req.query.limit) || 50)); try { const { data, error } = await adminSupabase .from("moderation_reports") .select( `*, reporter:user_profiles!moderation_reports_reporter_id_fkey ( id, full_name, username, avatar_url )`, ) .eq("status", status) .order("created_at", { ascending: false }) .limit(limit); if (error) return res.status(500).json({ error: error.message }); return res.json(data || []); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/moderation/reports/:id/status", async (req, res) => { const id = String(req.params.id || ""); const { status } = (req.body || {}) as { status?: string }; const allowed = ["open", "resolved", "ignored"]; if (!id || !status || !allowed.includes(String(status))) { return res.status(400).json({ error: "invalid input" }); } try { const { data, error } = await adminSupabase .from("moderation_reports") .update({ status }) .eq("id", id) .select() .single(); if (error) return res.status(500).json({ error: error.message }); return res.json(data); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // Creator Network API Routes // Get all creators with filters app.get("/api/creators", async (req, res) => { try { const arm = String(req.query.arm || "").trim(); const page = Math.max(1, Number(req.query.page) || 1); const limit = Math.max(1, Math.min(100, Number(req.query.limit) || 20)); const search = String(req.query.search || "").trim(); let query = adminSupabase .from("aethex_creators") .select( ` id, username, bio, skills, avatar_url, experience_level, arm_affiliations, primary_arm, created_at `, { count: "exact" }, ) .eq("is_discoverable", true) .order("created_at", { ascending: false }); if (arm) { query = query.contains("arm_affiliations", [arm]); } if (search) { query = query.or(`username.ilike.%${search}%,bio.ilike.%${search}%`); } const start = (page - 1) * limit; query = query.range(start, start + limit - 1); const { data, error, count } = await query; if (error) throw error; return res.json({ data, pagination: { page, limit, total: count || 0, pages: Math.ceil((count || 0) / limit), }, }); } catch (e: any) { console.error("[Creator API] Error fetching creators:", e?.message); return res.status(500).json({ error: "Failed to fetch creators" }); } }); // Get creator by username app.get("/api/creators/:identifier", async (req, res) => { try { const identifier = String(req.params.identifier || "").trim(); if (!identifier) { return res.status(400).json({ error: "identifier required" }); } // Check if identifier is a UUID (username-first, UUID fallback) const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( identifier, ); let query = adminSupabase .from("aethex_creators") .select( ` id, username, bio, skills, avatar_url, experience_level, arm_affiliations, primary_arm, created_at, updated_at `, ) .eq("is_discoverable", true); // Try username first (preferred), then UUID fallback if (isUUID) { query = query.eq("id", identifier); } else { query = query.eq("username", identifier); } const { data: creator, error } = await query.single(); // If username lookup failed and it's a valid UUID format, try UUID if (error && !isUUID) { if ( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( identifier, ) ) { const { data: creatorByUUID, error: uuidError } = await adminSupabase .from("aethex_creators") .select( ` id, username, bio, skills, avatar_url, experience_level, arm_affiliations, primary_arm, created_at, updated_at `, ) .eq("is_discoverable", true) .eq("id", identifier) .single(); if (!uuidError && creatorByUUID) { // Found by UUID, optionally redirect to username canonical URL const { data: devConnectLink } = await adminSupabase .from("aethex_devconnect_links") .select("devconnect_username, devconnect_profile_url") .eq("aethex_creator_id", creatorByUUID.id) .maybeSingle(); return res.json({ ...creatorByUUID, devconnect_link: devConnectLink, }); } } if (error.code === "PGRST116") { return res.status(404).json({ error: "Creator not found" }); } throw error; } if (error) { if (error.code === "PGRST116") { return res.status(404).json({ error: "Creator not found" }); } throw error; } const { data: devConnectLink } = await adminSupabase .from("aethex_devconnect_links") .select("devconnect_username, devconnect_profile_url") .eq("aethex_creator_id", creator.id) .maybeSingle(); return res.json({ ...creator, devconnect_link: devConnectLink, }); } catch (e: any) { console.error("[Creator API] Error fetching creator:", e?.message); return res.status(500).json({ error: "Failed to fetch creator" }); } }); // Get creator by user_id app.get("/api/creators/user/:userId", async (req, res) => { try { const userId = String(req.params.userId || "").trim(); if (!userId) { return res.status(400).json({ error: "userId required" }); } const { data: creator, error } = await adminSupabase .from("aethex_creators") .select( ` id, user_id, username, bio, skills, avatar_url, experience_level, arm_affiliations, primary_arm, spotify_profile_url, created_at, updated_at `, ) .eq("user_id", userId) .eq("is_discoverable", true) .maybeSingle(); if (!creator) { return res.status(404).json({ error: "Creator not found" }); } if (error) throw error; const { data: devConnectLink } = await adminSupabase .from("aethex_devconnect_links") .select("devconnect_username, devconnect_profile_url") .eq("aethex_creator_id", creator.id) .maybeSingle(); return res.json({ ...creator, devconnect_link: devConnectLink, }); } catch (e: any) { console.error( "[Creator API] Error fetching creator by user_id:", e?.message, ); return res.status(500).json({ error: "Failed to fetch creator" }); } }); // Create creator profile app.post("/api/creators", async (req, res) => { try { const { user_id, username, bio, skills, avatar_url, experience_level, primary_arm, arm_affiliations, } = req.body; if (!user_id || !username) { return res .status(400) .json({ error: "user_id and username required" }); } const { data, error } = await adminSupabase .from("aethex_creators") .insert({ user_id, username, bio: bio || null, skills: Array.isArray(skills) ? skills : [], avatar_url: avatar_url || null, experience_level: experience_level || null, primary_arm: primary_arm || null, arm_affiliations: Array.isArray(arm_affiliations) ? arm_affiliations : [], }) .select() .single(); if (error) { if (error.code === "23505") { return res.status(400).json({ error: "Username already taken" }); } throw error; } return res.status(201).json(data); } catch (e: any) { console.error("[Creator API] Error creating creator:", e?.message); return res .status(500) .json({ error: "Failed to create creator profile" }); } }); // Update creator profile app.put("/api/creators/:id", async (req, res) => { try { const creatorId = String(req.params.id || "").trim(); if (!creatorId) { return res.status(400).json({ error: "creator id required" }); } const { bio, skills, avatar_url, experience_level, primary_arm, arm_affiliations, is_discoverable, allow_recommendations, } = req.body; const { data, error } = await adminSupabase .from("aethex_creators") .update({ bio, skills: Array.isArray(skills) ? skills : undefined, avatar_url, experience_level, primary_arm, arm_affiliations: Array.isArray(arm_affiliations) ? arm_affiliations : undefined, is_discoverable, allow_recommendations, updated_at: new Date().toISOString(), }) .eq("id", creatorId) .select() .single(); if (error) throw error; return res.json(data); } catch (e: any) { console.error("[Creator API] Error updating creator:", e?.message); return res .status(500) .json({ error: "Failed to update creator profile" }); } }); // Get all opportunities with filters app.get("/api/opportunities", async (req, res) => { try { const arm = String(req.query.arm || "").trim(); const page = Math.max(1, Number(req.query.page) || 1); const limit = Math.max(1, Math.min(100, Number(req.query.limit) || 20)); const sort = String(req.query.sort || "recent"); const search = String(req.query.search || "").trim(); const jobType = String(req.query.jobType || "").trim(); const experienceLevel = String(req.query.experienceLevel || "").trim(); let query = adminSupabase .from("aethex_opportunities") .select( ` id, title, description, job_type, salary_min, salary_max, experience_level, arm_affiliation, posted_by_id, aethex_creators!aethex_opportunities_posted_by_id_fkey(username, avatar_url), status, created_at `, { count: "exact" }, ) .eq("status", "open"); if (arm) { query = query.eq("arm_affiliation", arm); } if (search) { query = query.or( `title.ilike.%${search}%,description.ilike.%${search}%`, ); } if (jobType) { query = query.eq("job_type", jobType); } if (experienceLevel) { query = query.eq("experience_level", experienceLevel); } const ascending = sort === "oldest"; query = query.order("created_at", { ascending }); const start = (page - 1) * limit; query = query.range(start, start + limit - 1); const { data, error, count } = await query; if (error) throw error; return res.json({ data, pagination: { page, limit, total: count || 0, pages: Math.ceil((count || 0) / limit), }, }); } catch (e: any) { console.error( "[Opportunities API] Error fetching opportunities:", e?.message, ); return res.status(500).json({ error: "Failed to fetch opportunities" }); } }); // Get opportunity by ID app.get("/api/opportunities/:id", async (req, res) => { try { const opportunityId = String(req.params.id || "").trim(); if (!opportunityId) { return res.status(400).json({ error: "opportunity id required" }); } const { data, error } = await adminSupabase .from("aethex_opportunities") .select( ` id, title, description, job_type, salary_min, salary_max, experience_level, arm_affiliation, posted_by_id, aethex_creators!aethex_opportunities_posted_by_id_fkey(id, username, bio, avatar_url), status, created_at, updated_at `, ) .eq("id", opportunityId) .eq("status", "open") .single(); if (error) { if (error.code === "PGRST116") { return res.status(404).json({ error: "Opportunity not found" }); } throw error; } return res.json(data); } catch (e: any) { console.error( "[Opportunities API] Error fetching opportunity:", e?.message, ); return res.status(500).json({ error: "Failed to fetch opportunity" }); } }); // Create opportunity app.post("/api/opportunities", async (req, res) => { try { const { user_id, title, description, job_type, salary_min, salary_max, experience_level, arm_affiliation, } = req.body; if (!user_id || !title) { return res.status(400).json({ error: "user_id and title required" }); } const { data: creator } = await adminSupabase .from("aethex_creators") .select("id") .eq("user_id", user_id) .single(); if (!creator) { return res.status(404).json({ error: "Creator profile not found. Create profile first.", }); } const { data, error } = await adminSupabase .from("aethex_opportunities") .insert({ title, description: description || null, job_type: job_type || null, salary_min: salary_min || null, salary_max: salary_max || null, experience_level: experience_level || null, arm_affiliation: arm_affiliation || null, posted_by_id: creator.id, status: "open", }) .select() .single(); if (error) throw error; return res.status(201).json(data); } catch (e: any) { console.error( "[Opportunities API] Error creating opportunity:", e?.message, ); return res.status(500).json({ error: "Failed to create opportunity" }); } }); // Update opportunity app.put("/api/opportunities/:id", async (req, res) => { try { const opportunityId = String(req.params.id || "").trim(); const { user_id, title, description, job_type, salary_min, salary_max, experience_level, status, } = req.body; if (!opportunityId || !user_id) { return res .status(400) .json({ error: "opportunity id and user_id required" }); } const { data: opportunity } = await adminSupabase .from("aethex_opportunities") .select("posted_by_id") .eq("id", opportunityId) .single(); if (!opportunity) { return res.status(404).json({ error: "Opportunity not found" }); } const { data: creator } = await adminSupabase .from("aethex_creators") .select("id") .eq("user_id", user_id) .single(); if (creator?.id !== opportunity.posted_by_id) { return res.status(403).json({ error: "Unauthorized" }); } const { data, error } = await adminSupabase .from("aethex_opportunities") .update({ title, description, job_type, salary_min, salary_max, experience_level, status, updated_at: new Date().toISOString(), }) .eq("id", opportunityId) .select() .single(); if (error) throw error; return res.json(data); } catch (e: any) { console.error( "[Opportunities API] Error updating opportunity:", e?.message, ); return res.status(500).json({ error: "Failed to update opportunity" }); } }); // Get my applications app.get("/api/applications", async (req, res) => { try { const userId = String(req.query.user_id || "").trim(); const page = Math.max(1, Number(req.query.page) || 1); const limit = Math.max(1, Math.min(100, Number(req.query.limit) || 10)); const status = String(req.query.status || "").trim(); if (!userId) { return res.status(400).json({ error: "user_id required" }); } const { data: creator } = await adminSupabase .from("aethex_creators") .select("id") .eq("user_id", userId) .single(); if (!creator) { return res.status(404).json({ error: "Creator profile not found" }); } let query = adminSupabase .from("aethex_applications") .select( ` id, creator_id, opportunity_id, status, cover_letter, response_message, applied_at, updated_at, aethex_opportunities(id, title, arm_affiliation, job_type, posted_by_id, aethex_creators!aethex_opportunities_posted_by_id_fkey(username, avatar_url)) `, { count: "exact" }, ) .eq("creator_id", creator.id); if (status) { query = query.eq("status", status); } query = query.order("applied_at", { ascending: false }); const start = (page - 1) * limit; query = query.range(start, start + limit - 1); const { data, error, count } = await query; if (error) throw error; return res.json({ data, pagination: { page, limit, total: count || 0, pages: Math.ceil((count || 0) / limit), }, }); } catch (e: any) { console.error( "[Applications API] Error fetching applications:", e?.message, ); return res.status(500).json({ error: "Failed to fetch applications" }); } }); // Submit application app.post("/api/applications", async (req, res) => { try { const { user_id, opportunity_id, cover_letter } = req.body; if (!user_id || !opportunity_id) { return res .status(400) .json({ error: "user_id and opportunity_id required" }); } const { data: creator } = await adminSupabase .from("aethex_creators") .select("id") .eq("user_id", user_id) .single(); if (!creator) { return res.status(404).json({ error: "Creator profile not found" }); } const { data: opportunity } = await adminSupabase .from("aethex_opportunities") .select("id") .eq("id", opportunity_id) .eq("status", "open") .single(); if (!opportunity) { return res .status(404) .json({ error: "Opportunity not found or closed" }); } const { data: existing } = await adminSupabase .from("aethex_applications") .select("id") .eq("creator_id", creator.id) .eq("opportunity_id", opportunity_id) .maybeSingle(); if (existing) { return res .status(400) .json({ error: "You have already applied to this opportunity" }); } const { data, error } = await adminSupabase .from("aethex_applications") .insert({ creator_id: creator.id, opportunity_id, cover_letter: cover_letter || null, status: "submitted", }) .select() .single(); if (error) throw error; // Notify opportunity poster of new application if (opportunity?.posted_by_id) { try { const { data: creatorProfile } = await adminSupabase .from("aethex_creators") .select("user_id, full_name") .eq("id", creator.id) .single(); await adminSupabase.from("notifications").insert({ user_id: opportunity.posted_by_id, type: "info", title: `šŸ“‹ New Application: ${opportunity.title}`, message: `${creatorProfile?.full_name || "A creator"} applied for your opportunity.`, }); } catch (notifError) { console.warn( "Failed to create application notification:", notifError, ); } } return res.status(201).json(data); } catch (e: any) { console.error( "[Applications API] Error submitting application:", e?.message, ); return res.status(500).json({ error: "Failed to submit application" }); } }); // Update application status app.put("/api/applications/:id", async (req, res) => { try { const applicationId = String(req.params.id || "").trim(); const { user_id, status, response_message } = req.body; if (!applicationId || !user_id) { return res .status(400) .json({ error: "application id and user_id required" }); } const { data: application } = await adminSupabase .from("aethex_applications") .select( ` id, opportunity_id, aethex_opportunities(posted_by_id) `, ) .eq("id", applicationId) .single(); if (!application) { return res.status(404).json({ error: "Application not found" }); } const { data: creator } = await adminSupabase .from("aethex_creators") .select("id") .eq("user_id", user_id) .single(); if (creator?.id !== application.aethex_opportunities.posted_by_id) { return res.status(403).json({ error: "Unauthorized" }); } const { data, error } = await adminSupabase .from("aethex_applications") .update({ status, response_message: response_message || null, updated_at: new Date().toISOString(), }) .eq("id", applicationId) .select() .single(); if (error) throw error; // Notify applicant of status change if (data) { try { const { data: applicantProfile } = await adminSupabase .from("aethex_creators") .select("user_id") .eq("id", data.creator_id) .single(); if (applicantProfile?.user_id) { const statusEmoji = status === "accepted" ? "āœ…" : status === "rejected" ? "āŒ" : "šŸ“"; const statusMessage = status === "accepted" ? "accepted" : status === "rejected" ? "rejected" : "updated"; await adminSupabase.from("notifications").insert({ user_id: applicantProfile.user_id, type: status === "accepted" ? "success" : status === "rejected" ? "error" : "info", title: `${statusEmoji} Application ${statusMessage}`, message: response_message || `Your application has been ${statusMessage}.`, }); } } catch (notifError) { console.warn("Failed to create status notification:", notifError); } } return res.json(data); } catch (e: any) { console.error( "[Applications API] Error updating application:", e?.message, ); return res.status(500).json({ error: "Failed to update application" }); } }); // Withdraw application app.delete("/api/applications/:id", async (req, res) => { try { const applicationId = String(req.params.id || "").trim(); const { user_id } = req.body; if (!applicationId || !user_id) { return res .status(400) .json({ error: "application id and user_id required" }); } const { data: application } = await adminSupabase .from("aethex_applications") .select("creator_id") .eq("id", applicationId) .single(); if (!application) { return res.status(404).json({ error: "Application not found" }); } const { data: creator } = await adminSupabase .from("aethex_creators") .select("id") .eq("user_id", user_id) .single(); if (creator?.id !== application.creator_id) { return res.status(403).json({ error: "Unauthorized" }); } const { error } = await adminSupabase .from("aethex_applications") .delete() .eq("id", applicationId); if (error) throw error; return res.json({ ok: true }); } catch (e: any) { console.error( "[Applications API] Error withdrawing application:", e?.message, ); return res .status(500) .json({ error: "Failed to withdraw application" }); } }); // Link DevConnect account app.post("/api/devconnect/link", async (req, res) => { try { const { user_id, devconnect_username, devconnect_profile_url } = req.body; if (!user_id || !devconnect_username) { return res .status(400) .json({ error: "user_id and devconnect_username required" }); } const { data: creator } = await adminSupabase .from("aethex_creators") .select("id") .eq("user_id", user_id) .single(); if (!creator) { return res.status(404).json({ error: "Creator profile not found. Create profile first.", }); } const { data: existing } = await adminSupabase .from("aethex_devconnect_links") .select("id") .eq("aethex_creator_id", creator.id) .maybeSingle(); let result; let status = 201; if (existing) { const { data, error } = await adminSupabase .from("aethex_devconnect_links") .update({ devconnect_username, devconnect_profile_url: devconnect_profile_url || `https://dev-link.me/${devconnect_username}`, }) .eq("aethex_creator_id", creator.id) .select() .single(); if (error) throw error; result = data; status = 200; } else { const { data, error } = await adminSupabase .from("aethex_devconnect_links") .insert({ aethex_creator_id: creator.id, devconnect_username, devconnect_profile_url: devconnect_profile_url || `https://dev-link.me/${devconnect_username}`, }) .select() .single(); if (error) throw error; result = data; } await adminSupabase .from("aethex_creators") .update({ devconnect_linked: true }) .eq("id", creator.id); return res.status(status).json(result); } catch (e: any) { console.error("[DevConnect API] Error linking account:", e?.message); return res .status(500) .json({ error: "Failed to link DevConnect account" }); } }); // Get DevConnect link app.get("/api/devconnect/link", async (req, res) => { try { const userId = String(req.query.user_id || "").trim(); if (!userId) { return res.status(400).json({ error: "user_id required" }); } const { data: creator } = await adminSupabase .from("aethex_creators") .select("id") .eq("user_id", userId) .single(); if (!creator) { return res.status(404).json({ error: "Creator profile not found" }); } const { data, error } = await adminSupabase .from("aethex_devconnect_links") .select("*") .eq("aethex_creator_id", creator.id) .maybeSingle(); if (error && error.code !== "PGRST116") { throw error; } return res.json({ data: data || null }); } catch (e: any) { console.error("[DevConnect API] Error fetching link:", e?.message); return res .status(500) .json({ error: "Failed to fetch DevConnect link" }); } }); // Unlink DevConnect account app.delete("/api/devconnect/link", async (req, res) => { try { const { user_id } = req.body; if (!user_id) { return res.status(400).json({ error: "user_id required" }); } const { data: creator } = await adminSupabase .from("aethex_creators") .select("id") .eq("user_id", user_id) .single(); if (!creator) { return res.status(404).json({ error: "Creator profile not found" }); } const { error } = await adminSupabase .from("aethex_devconnect_links") .delete() .eq("aethex_creator_id", creator.id); if (error) throw error; await adminSupabase .from("aethex_creators") .update({ devconnect_linked: false }) .eq("id", creator.id); return res.json({ ok: true }); } catch (e: any) { console.error("[DevConnect API] Error unlinking account:", e?.message); return res .status(500) .json({ error: "Failed to unlink DevConnect account" }); } }); // Ethos Tracks API app.get("/api/ethos/tracks", async (req, res) => { try { const { limit = 50, offset = 0, genre, licenseType, search, } = req.query; let dbQuery = adminSupabase .from("ethos_tracks") .select( ` id, user_id, title, description, file_url, duration_seconds, genre, license_type, bpm, is_published, download_count, rating, price, created_at, updated_at, user_profiles(id, full_name, avatar_url) `, { count: "exact" }, ) .eq("is_published", true) .order("created_at", { ascending: false }); if (genre) dbQuery = dbQuery.contains("genre", [genre]); if (licenseType) dbQuery = dbQuery.eq("license_type", licenseType); if (search) { dbQuery = dbQuery.or( `title.ilike.%${search}%,description.ilike.%${search}%`, ); } const { data, error, count } = await dbQuery.range( Number(offset), Number(offset) + Number(limit) - 1, ); if (error) throw error; res.json({ data: data || [], total: count, limit: Number(limit), offset: Number(offset), }); } catch (e: any) { console.error("[Ethos Tracks API] Error fetching tracks:", e?.message); res.status(500).json({ error: e?.message || "Failed to fetch tracks" }); } }); app.post("/api/ethos/tracks", async (req, res) => { try { const userId = req.headers["x-user-id"] || req.body?.user_id; if (!userId) { return res.status(401).json({ error: "Unauthorized" }); } const { title, description, file_url, duration_seconds, genre, license_type, bpm, is_published, price, } = req.body; if (!title || !file_url || !license_type) { return res.status(400).json({ error: "Missing required fields: title, file_url, license_type", }); } const { data, error } = await adminSupabase .from("ethos_tracks") .insert([ { user_id: userId, title, description, file_url, duration_seconds, genre: genre || [], license_type, bpm, is_published: is_published !== false, price, }, ]) .select(); if (error) throw error; if (license_type === "ecosystem" && data && data[0]) { const trackId = data[0].id; await adminSupabase.from("ethos_ecosystem_licenses").insert([ { track_id: trackId, artist_id: userId, accepted_at: new Date().toISOString(), }, ]); } res.status(201).json(data[0]); } catch (e: any) { console.error("[Ethos Tracks API] Error creating track:", e?.message); res.status(500).json({ error: e?.message || "Failed to create track" }); } }); // Ethos Artists API app.get("/api/ethos/artists", async (req, res) => { try { const { id, limit = 50, offset = 0, verified, forHire } = req.query; if (id) { // Get single artist by ID const { data: artist, error: artistError } = await adminSupabase .from("ethos_artist_profiles") .select( ` user_id, skills, for_hire, bio, portfolio_url, sample_price_track, sample_price_sfx, sample_price_score, turnaround_days, verified, total_downloads, created_at, user_profiles(id, full_name, avatar_url) `, ) .eq("user_id", id) .single(); if (artistError && artistError.code !== "PGRST116") throw artistError; if (!artist) { return res.status(404).json({ error: "Artist not found" }); } const { data: tracks } = await adminSupabase .from("ethos_tracks") .select("*") .eq("user_id", id) .eq("is_published", true) .order("created_at", { ascending: false }); return res.json({ ...artist, tracks: tracks || [], }); } // Get list of artists let dbQuery = adminSupabase.from("ethos_artist_profiles").select( ` user_id, skills, for_hire, bio, portfolio_url, sample_price_track, sample_price_sfx, sample_price_score, turnaround_days, verified, total_downloads, created_at, user_profiles(id, full_name, avatar_url) `, { count: "exact" }, ); if (verified === "true") dbQuery = dbQuery.eq("verified", true); if (forHire === "true") dbQuery = dbQuery.eq("for_hire", true); const { data, error, count } = await dbQuery .order("verified", { ascending: false }) .order("total_downloads", { ascending: false }) .range(Number(offset), Number(offset) + Number(limit) - 1); if (error) throw error; res.json({ data: data || [], total: count, limit: Number(limit), offset: Number(offset), }); } catch (e: any) { console.error( "[Ethos Artists API] Error fetching artists:", e?.message, ); res .status(500) .json({ error: e?.message || "Failed to fetch artists" }); } }); app.put("/api/ethos/artists", async (req, res) => { try { const userId = req.headers["x-user-id"] || req.body?.user_id; if (!userId) { return res.status(401).json({ error: "Unauthorized" }); } const { skills, for_hire, bio, portfolio_url, sample_price_track, sample_price_sfx, sample_price_score, turnaround_days, } = req.body; const { data, error } = await adminSupabase .from("ethos_artist_profiles") .upsert( { user_id: userId, skills: skills || [], for_hire: for_hire !== false, bio, portfolio_url, sample_price_track, sample_price_sfx, sample_price_score, turnaround_days, }, { onConflict: "user_id" }, ) .select(); if (error) throw error; res.json(data[0]); } catch (e: any) { console.error("[Ethos Artists API] Error updating artist:", e?.message); res .status(500) .json({ error: e?.message || "Failed to update artist profile" }); } }); // Task assignment with notification app.post("/api/tasks", async (req, res) => { try { const { project_id, title, description, assignee_id, due_date, user_id, } = req.body; if (!project_id || !title || !user_id) { return res .status(400) .json({ error: "project_id, title, and user_id required" }); } const { data, error } = await adminSupabase .from("project_tasks") .insert({ project_id, title, description: description || null, assignee_id: assignee_id || null, due_date: due_date || null, status: "open", }) .select() .single(); if (error) throw error; // Notify assignee if assigned if (assignee_id && assignee_id !== user_id) { try { const { data: assigner } = await adminSupabase .from("user_profiles") .select("full_name, username") .eq("id", user_id) .single(); const assignerName = (assigner as any)?.full_name || (assigner as any)?.username || "Someone"; await adminSupabase.from("notifications").insert({ user_id: assignee_id, type: "info", title: "šŸ“‹ Task assigned to you", message: `${assignerName} assigned you a task: "${title}"`, }); } catch (notifError) { console.warn("Failed to create task notification:", notifError); } } return res.status(201).json(data); } catch (e: any) { console.error("[Tasks API] Error creating task:", e?.message); return res.status(500).json({ error: "Failed to create task" }); } }); // Assign task with notification app.put("/api/tasks/:id/assign", async (req, res) => { try { const taskId = String(req.params.id || "").trim(); const { assignee_id, user_id } = req.body; if (!taskId || !assignee_id || !user_id) { return res .status(400) .json({ error: "task id, assignee_id, and user_id required" }); } const { data: task } = await adminSupabase .from("project_tasks") .select("title") .eq("id", taskId) .single(); if (!task) { return res.status(404).json({ error: "Task not found" }); } const { data, error } = await adminSupabase .from("project_tasks") .update({ assignee_id }) .eq("id", taskId) .select() .single(); if (error) throw error; // Notify assignee try { const { data: assigner } = await adminSupabase .from("user_profiles") .select("full_name, username") .eq("id", user_id) .single(); const assignerName = (assigner as any)?.full_name || (assigner as any)?.username || "Someone"; await adminSupabase.from("notifications").insert({ user_id: assignee_id, type: "info", title: "šŸ“‹ Task assigned to you", message: `${assignerName} assigned you: "${task.title}"`, }); } catch (notifError) { console.warn("Failed to create assignment notification:", notifError); } return res.json(data); } catch (e: any) { console.error("[Tasks API] Error assigning task:", e?.message); return res.status(500).json({ error: "Failed to assign task" }); } }); // Moderation report with staff notification app.post("/api/moderation/reports", async (req, res) => { try { const { reported_user_id, report_type, description, reporter_id } = req.body; if (!reported_user_id || !report_type || !reporter_id) { return res.status(400).json({ error: "reported_user_id, report_type, and reporter_id required", }); } const { data, error } = await adminSupabase .from("moderation_reports") .insert({ reported_user_id, report_type, description: description || null, reporter_id, status: "pending", }) .select() .single(); if (error) throw error; // Notify staff members try { const { data: staffUsers } = await adminSupabase .from("user_roles") .select("user_id") .in("role", ["owner", "admin", "moderator"]); if (staffUsers && staffUsers.length > 0) { const notifications = staffUsers.map((staff: any) => ({ user_id: staff.user_id, type: "warning", title: "🚨 New moderation report", message: `A ${report_type} report has been submitted. Please review.`, })); await adminSupabase.from("notifications").insert(notifications); } } catch (notifError) { console.warn("Failed to notify staff:", notifError); } return res.status(201).json(data); } catch (e: any) { console.error("[Moderation API] Error creating report:", e?.message); return res.status(500).json({ error: "Failed to create report" }); } }); // Staff Members API app.get("/api/staff/members", async (_req, res) => { try { console.log( "[Staff] GET /api/staff/members - adminSupabase initialized:", !!adminSupabase, ); if (!adminSupabase) { console.error("[Staff] adminSupabase is not initialized"); return res.status(500).json({ error: "Supabase client not initialized", message: "SUPABASE_URL or SUPABASE_SERVICE_ROLE not set", }); } const { data, error } = await adminSupabase .from("staff_members") .select("*") .order("full_name", { ascending: true }); if (error) { console.error("[Staff] Error fetching staff members:", error); if (isTableMissing(error)) { console.log("[Staff] Table not found, returning empty array"); return res.json([]); } return res.status(500).json({ error: error.message }); } console.log( "[Staff] Successfully fetched", (data || []).length, "staff members", ); return res.json(data || []); } catch (e: any) { console.error("[Staff] Unexpected error:", e); return res.status(500).json({ error: e?.message || String(e) }); } }); app.get("/api/staff/members/seed", async (req, res) => { try { // Ensure response headers are set correctly res.setHeader("Content-Type", "application/json"); if (!adminSupabase) { console.error("[Staff Seed] adminSupabase is not initialized"); return res.status(500).json({ error: "Supabase client not initialized", message: "SUPABASE_URL or SUPABASE_SERVICE_ROLE not set", }); } // First, check if table exists and is accessible const { data: existingData, error: existingError } = await adminSupabase .from("staff_members") .select("count") .limit(1); if (existingError) { console.error("[Staff Seed] Table check error:", existingError); return res.status(500).json({ error: "Cannot access staff_members table", tableError: existingError, }); } console.log("[Staff Seed] Table exists and is accessible"); const mockMembers = [ { email: "alex@aethex.dev", full_name: "Alex Chen", position: "Founder & CEO", department: "Executive", phone: "+1 (555) 000-0001", role: "owner", location: "San Francisco, CA", }, { email: "jordan@aethex.dev", full_name: "Jordan Martinez", position: "CTO", department: "Engineering", phone: "+1 (555) 000-0002", role: "admin", location: "New York, NY", }, { email: "sam@aethex.dev", full_name: "Sam Patel", position: "Community Manager", department: "Community", phone: "+1 (555) 000-0003", role: "staff", location: "Austin, TX", }, { email: "taylor@aethex.dev", full_name: "Taylor Kim", position: "Operations Lead", department: "Operations", phone: "+1 (555) 000-0004", role: "staff", location: "Seattle, WA", }, { email: "casey@aethex.dev", full_name: "Casey Zhang", position: "Software Engineer", department: "Engineering", phone: "+1 (555) 000-0005", role: "employee", location: "San Francisco, CA", }, { email: "morgan@aethex.dev", full_name: "Morgan Lee", position: "Designer", department: "Design", phone: "+1 (555) 000-0006", role: "employee", location: "Los Angeles, CA", }, { email: "alex.kim@aethex.dev", full_name: "Alex Kim", position: "Marketing Manager", department: "Marketing", phone: "+1 (555) 000-0007", role: "staff", location: "Boston, MA", }, { email: "jordan.lee@aethex.dev", full_name: "Jordan Lee", position: "Data Analyst", department: "Analytics", phone: "+1 (555) 000-0008", role: "employee", location: "Denver, CO", }, ]; try { const { data, error } = await adminSupabase .from("staff_members") .upsert(mockMembers, { onConflict: "email" }) .select(); if (error) { console.error("[Staff Seed Error] Supabase error:", error); return res.status(500).json({ error: "Failed to seed staff members", supabaseError: error, mockMembersCount: mockMembers.length, }); } console.log( "[Staff Seed Success] Inserted", data?.length || 0, "members", ); const response = { success: true, count: data?.length || 0, members: data || [], }; return res.status(201).json(response); } catch (insertError: any) { console.error("[Staff Seed Insert Error]", insertError); return res.status(500).json({ error: "Exception during insert", message: insertError?.message || String(insertError), }); } } catch (e: any) { console.error("[Staff Seed Exception]", e); res.status(500).json({ error: e?.message || String(e) }); } }); app.post("/api/staff/members", async (req, res) => { try { const { user_id, email, full_name, position, department, phone, avatar_url, role, hired_date, } = req.body || {}; if (!email || !full_name) { return res.status(400).json({ error: "Missing required fields: email, full_name", }); } const { data, error } = await adminSupabase .from("staff_members") .insert([ { user_id: user_id || null, email, full_name, position: position || null, department: department || null, phone: phone || null, avatar_url: avatar_url || null, role: role || "employee", hired_date: hired_date || null, }, ]) .select(); if (error) { return res.status(500).json({ error: "Failed to create staff member", details: error.message, }); } return res.status(201).json(data?.[0] || {}); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); app.get("/api/staff/members-detail", async (req, res) => { try { const id = String(req.query.id || ""); if (!id) { return res.status(400).json({ error: "Missing staff member ID" }); } const { data, error } = await adminSupabase .from("staff_members") .select("*") .eq("id", id) .single(); if (error || !data) { return res.status(404).json({ error: "Staff member not found" }); } return res.json(data); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); app.put("/api/staff/members-detail", async (req, res) => { try { const id = String(req.query.id || ""); if (!id) { return res.status(400).json({ error: "Missing staff member ID" }); } const updates = req.body || {}; const { data, error } = await adminSupabase .from("staff_members") .update({ ...updates, updated_at: new Date().toISOString(), }) .eq("id", id) .select() .single(); if (error) { return res.status(500).json({ error: "Failed to update staff member", details: error.message, }); } if (!data) { return res.status(404).json({ error: "Staff member not found" }); } return res.json(data); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); app.delete("/api/staff/members-detail", async (req, res) => { try { const id = String(req.query.id || ""); if (!id) { return res.status(400).json({ error: "Missing staff member ID" }); } const { error } = await adminSupabase .from("staff_members") .delete() .eq("id", id); if (error) { return res.status(500).json({ error: "Failed to delete staff member", details: error.message, }); } return res.json({ success: true, id }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // Track device login and send security alert app.post("/api/auth/login-device", async (req, res) => { try { const { user_id, device_name, ip_address, user_agent } = req.body; if (!user_id) { return res.status(400).json({ error: "user_id required" }); } // Store device login const { data: sessionData } = await adminSupabase .from("game_sessions") .insert({ user_id, device_id: device_name || "Unknown Device", ip_address: ip_address || null, user_agent: user_agent || null, session_token: randomUUID(), }) .select() .single(); // Check if this is a new device (security alert) const { data: previousSessions } = await adminSupabase .from("game_sessions") .select("device_id") .eq("user_id", user_id) .neq("device_id", device_name || "Unknown Device"); // If new device, send security notification if (!previousSessions || previousSessions.length === 0) { try { await adminSupabase.from("notifications").insert({ user_id, type: "warning", title: "šŸ” New device login detected", message: `New login from ${device_name || "Unknown device"} at ${ip_address || "Unknown location"}. If this wasn't you, please secure your account.`, }); } catch (notifError) { console.warn("Failed to create security notification:", notifError); } } return res.status(201).json(sessionData); } catch (e: any) { console.error("[Auth API] Error tracking login:", e?.message); return res.status(500).json({ error: "Failed to track login" }); } }); // Admin endpoint to delete user account app.delete("/api/admin/users/delete", async (req, res) => { try { const adminToken = req.headers.authorization?.replace("Bearer ", "") || ""; if (adminToken !== process.env.DISCORD_ADMIN_REGISTER_TOKEN) { return res.status(401).json({ error: "Unauthorized" }); } const { email } = req.body; if (!email) { return res.status(400).json({ error: "Missing email parameter" }); } // Get the user by email const { data: profile, error: profileError } = await adminSupabase .from("user_profiles") .select("user_id, email") .eq("email", email) .single(); if (profileError || !profile) { return res.status(404).json({ error: "User not found", details: profileError?.message, }); } const userId = profile.user_id; // Delete from various tables in order await adminSupabase .from("achievements_earned") .delete() .eq("user_id", userId); await adminSupabase.from("applications").delete().eq("user_id", userId); await adminSupabase .from("creator_profiles") .delete() .eq("user_id", userId); await adminSupabase.from("projects").delete().eq("user_id", userId); await adminSupabase.from("social_posts").delete().eq("user_id", userId); await adminSupabase .from("user_email_links") .delete() .eq("user_id", userId); await adminSupabase .from("discord_links") .delete() .eq("user_id", userId); await adminSupabase.from("web3_wallets").delete().eq("user_id", userId); // Delete user profile const { error: profileDeleteError } = await adminSupabase .from("user_profiles") .delete() .eq("user_id", userId); if (profileDeleteError) { return res.status(500).json({ error: "Failed to delete user profile", details: profileDeleteError.message, }); } // Delete from Supabase auth try { await (adminSupabase.auth.admin as any).deleteUser(userId); } catch (authError: any) { console.warn("Auth deletion warning:", authError?.message); } return res.json({ success: true, message: `User account ${email} has been successfully deleted`, userId, }); } catch (e: any) { console.error("[Admin API] Error deleting user:", e?.message); return res.status(500).json({ error: "Internal server error", message: e?.message, }); } }); } catch (e) { console.warn("Admin API not initialized:", e); } // ======================================== // NEXUS MARKETPLACE API ENDPOINTS // ======================================== // Helper function to get user from token const getUserFromToken = async (req: express.Request) => { const authHeader = req.headers.authorization; if (!authHeader) return null; const token = authHeader.replace("Bearer ", ""); const { data: { user }, error } = await adminSupabase.auth.getUser(token); if (error || !user) return null; return user; }; // GET/POST /api/nexus/creator/profile app.get("/api/nexus/creator/profile", async (req, res) => { const user = await getUserFromToken(req); if (!user) return res.status(401).json({ error: "Unauthorized" }); try { const { data: profile, error: profileError } = await adminSupabase .from("nexus_creator_profiles") .select("*") .eq("user_id", user.id) .single(); if (profileError && profileError.code !== "PGRST116") { return res.status(500).json({ error: profileError.message }); } if (!profile) { return res.status(200).json({ user_id: user.id, headline: "", bio: "", profile_image_url: null, skills: [], experience_level: "intermediate", hourly_rate: null, portfolio_url: null, availability_status: "available", availability_hours_per_week: null, verified: false, total_earnings: 0, rating: null, review_count: 0, created_at: null, updated_at: null, }); } return res.status(200).json(profile); } catch (error: any) { return res.status(500).json({ error: error?.message || "Server error" }); } }); app.post("/api/nexus/creator/profile", async (req, res) => { const user = await getUserFromToken(req); if (!user) return res.status(401).json({ error: "Unauthorized" }); try { const { headline, bio, profile_image_url, skills, experience_level, hourly_rate, portfolio_url, availability_status, availability_hours_per_week, } = req.body; const { data: profile, error: upsertError } = await adminSupabase .from("nexus_creator_profiles") .upsert({ user_id: user.id, headline: headline || null, bio: bio || null, profile_image_url: profile_image_url || null, skills: Array.isArray(skills) ? skills : [], experience_level: experience_level || "intermediate", hourly_rate: hourly_rate || null, portfolio_url: portfolio_url || null, availability_status: availability_status || "available", availability_hours_per_week: availability_hours_per_week || null, }, { onConflict: "user_id" }) .select() .single(); if (upsertError) { return res.status(500).json({ error: upsertError.message }); } return res.status(200).json(profile); } catch (error: any) { return res.status(500).json({ error: error?.message || "Server error" }); } }); // GET /api/nexus/creator/applications app.get("/api/nexus/creator/applications", async (req, res) => { const user = await getUserFromToken(req); if (!user) return res.status(401).json({ error: "Unauthorized" }); try { const status = req.query.status as string | undefined; const limit = parseInt((req.query.limit as string) || "50", 10); const offset = parseInt((req.query.offset as string) || "0", 10); let query = adminSupabase .from("nexus_applications") .select(` *, opportunity:nexus_opportunities( id, title, description, category, budget_type, budget_min, budget_max, timeline_type, status, posted_by, created_at ) `) .eq("creator_id", user.id) .order("created_at", { ascending: false }); if (status) { query = query.eq("status", status); } const { data: applications, error: appError } = await query.range(offset, offset + limit - 1); if (appError) { return res.status(500).json({ error: appError.message }); } return res.status(200).json({ applications: applications || [], total: applications?.length || 0, limit, offset, }); } catch (error: any) { return res.status(500).json({ error: error?.message || "Server error" }); } }); // GET /api/nexus/creator/contracts app.get("/api/nexus/creator/contracts", async (req, res) => { const user = await getUserFromToken(req); if (!user) return res.status(401).json({ error: "Unauthorized" }); try { const status = req.query.status as string | undefined; const limit = parseInt((req.query.limit as string) || "50", 10); const offset = parseInt((req.query.offset as string) || "0", 10); let query = adminSupabase .from("nexus_contracts") .select(` *, client:user_profiles(id, full_name, avatar_url), milestones:nexus_milestones(*), payments:nexus_payments(*) `) .eq("creator_id", user.id) .order("created_at", { ascending: false }); if (status) { query = query.eq("status", status); } const { data: contracts, error: contractsError } = await query.range(offset, offset + limit - 1); if (contractsError) { return res.status(500).json({ error: contractsError.message }); } return res.status(200).json({ contracts: contracts || [], total: contracts?.length || 0, limit, offset, }); } catch (error: any) { return res.status(500).json({ error: error?.message || "Server error" }); } }); // GET /api/nexus/creator/payouts app.get("/api/nexus/creator/payouts", async (req, res) => { const user = await getUserFromToken(req); if (!user) return res.status(401).json({ error: "Unauthorized" }); try { const status = req.query.status as string | undefined; const limit = parseInt((req.query.limit as string) || "50", 10); const offset = parseInt((req.query.offset as string) || "0", 10); // First get user's contracts to ensure we only return their payments const { data: userContracts } = await adminSupabase .from("nexus_contracts") .select("id, total_amount, creator_payout_amount, status") .eq("creator_id", user.id); if (!userContracts || userContracts.length === 0) { return res.status(200).json({ payments: [], summary: { total_earnings: 0, pending_payouts: 0, completed_contracts: 0, }, limit, offset, }); } const contractIds = userContracts.map((c: any) => c.id); let query = adminSupabase .from("nexus_payments") .select(` *, contract:nexus_contracts(id, title, total_amount, status, client_id, created_at), milestone:nexus_milestones(id, description, amount, status) `) .in("contract_id", contractIds) .order("created_at", { ascending: false }); if (status) { query = query.eq("payment_status", status); } const { data: payments, error: paymentsError } = await query.range(offset, offset + limit - 1); if (paymentsError) { return res.status(500).json({ error: paymentsError.message }); } // Calculate summary stats from already-fetched user contracts const totalEarnings = userContracts.reduce((sum: number, c: any) => sum + (c.creator_payout_amount || 0), 0); const completedContracts = userContracts.filter((c: any) => c.status === "completed").length; const pendingPayouts = (payments || []) .filter((p: any) => p.payment_status === "pending") .reduce((sum: number, p: any) => sum + (p.creator_payout || 0), 0); return res.status(200).json({ payments: payments || [], summary: { total_earnings: totalEarnings, pending_payouts: pendingPayouts, completed_contracts: completedContracts, }, limit, offset, }); } catch (error: any) { return res.status(500).json({ error: error?.message || "Server error" }); } }); // GET/POST /api/nexus/client/opportunities app.get("/api/nexus/client/opportunities", async (req, res) => { const user = await getUserFromToken(req); if (!user) return res.status(401).json({ error: "Unauthorized" }); try { const status = req.query.status as string | undefined; const limit = parseInt((req.query.limit as string) || "50", 10); const offset = parseInt((req.query.offset as string) || "0", 10); let query = adminSupabase .from("nexus_opportunities") .select(` *, applications:nexus_applications(id, status, creator_id) `) .eq("posted_by", user.id) .order("created_at", { ascending: false }); if (status) { query = query.eq("status", status); } const { data: opportunities, error: oppError } = await query.range(offset, offset + limit - 1); if (oppError) { return res.status(500).json({ error: oppError.message }); } return res.status(200).json({ opportunities: opportunities || [], limit, offset, }); } catch (error: any) { return res.status(500).json({ error: error?.message || "Server error" }); } }); app.post("/api/nexus/client/opportunities", async (req, res) => { const user = await getUserFromToken(req); if (!user) return res.status(401).json({ error: "Unauthorized" }); try { const { title, description, category, required_skills, budget_type, budget_min, budget_max, timeline_type, duration_weeks, location_requirement, required_experience, company_name, } = req.body; if (!title || !description || !category || !budget_type) { return res.status(400).json({ error: "Missing required fields: title, description, category, budget_type", }); } const { data: opportunity, error: createError } = await adminSupabase .from("nexus_opportunities") .insert({ posted_by: user.id, title, description, category, required_skills: Array.isArray(required_skills) ? required_skills : [], budget_type, budget_min: budget_min || null, budget_max: budget_max || null, timeline_type: timeline_type || "flexible", duration_weeks: duration_weeks || null, location_requirement: location_requirement || "remote", required_experience: required_experience || "any", company_name: company_name || null, status: "open", published_at: new Date().toISOString(), }) .select() .single(); if (createError) { return res.status(500).json({ error: createError.message }); } return res.status(201).json(opportunity); } catch (error: any) { return res.status(500).json({ error: error?.message || "Server error" }); } }); // GET /api/nexus/client/applicants app.get("/api/nexus/client/applicants", async (req, res) => { const user = await getUserFromToken(req); if (!user) return res.status(401).json({ error: "Unauthorized" }); try { const opportunityId = req.query.opportunity_id as string | undefined; const status = req.query.status as string | undefined; const limit = parseInt((req.query.limit as string) || "50", 10); const offset = parseInt((req.query.offset as string) || "0", 10); // If no opportunity_id, return all applicants across all user's opportunities if (!opportunityId) { // Get all opportunities for this user const { data: userOpps } = await adminSupabase .from("nexus_opportunities") .select("id") .eq("posted_by", user.id); if (!userOpps || userOpps.length === 0) { return res.status(200).json({ applicants: [], limit, offset, total: 0, }); } const oppIds = userOpps.map((o: any) => o.id); let query = adminSupabase .from("nexus_applications") .select(` *, creator:user_profiles(id, full_name, avatar_url), creator_profile:nexus_creator_profiles(skills, experience_level, hourly_rate, rating, review_count), opportunity:nexus_opportunities(id, title) `) .in("opportunity_id", oppIds) .order("created_at", { ascending: false }); if (status) { query = query.eq("status", status); } const { data: applications, error: appError } = await query.range(offset, offset + limit - 1); if (appError) { return res.status(500).json({ error: appError.message }); } return res.status(200).json({ applicants: applications || [], limit, offset, total: applications?.length || 0, }); } // Verify client owns this opportunity const { data: opportunity, error: oppError } = await adminSupabase .from("nexus_opportunities") .select("id, posted_by") .eq("id", opportunityId) .single(); if (oppError || !opportunity) { return res.status(404).json({ error: "Opportunity not found" }); } if (opportunity.posted_by !== user.id) { return res.status(403).json({ error: "Unauthorized" }); } let query = adminSupabase .from("nexus_applications") .select(` *, creator:user_profiles(id, full_name, avatar_url), creator_profile:nexus_creator_profiles(skills, experience_level, hourly_rate, rating, review_count) `) .eq("opportunity_id", opportunityId) .order("created_at", { ascending: false }); if (status) { query = query.eq("status", status); } const { data: applications, error: appError } = await query.range(offset, offset + limit - 1); if (appError) { return res.status(500).json({ error: appError.message }); } return res.status(200).json({ applicants: applications || [], limit, offset, total: applications?.length || 0, }); } catch (error: any) { return res.status(500).json({ error: error?.message || "Server error" }); } }); // GET /api/nexus/client/payment-history app.get("/api/nexus/client/payment-history", async (req, res) => { const user = await getUserFromToken(req); if (!user) return res.status(401).json({ error: "Unauthorized" }); try { const limit = parseInt((req.query.limit as string) || "50", 10); const offset = parseInt((req.query.offset as string) || "0", 10); // First get user's contracts where they are the client const { data: clientContracts } = await adminSupabase .from("nexus_contracts") .select("id") .eq("client_id", user.id); if (!clientContracts || clientContracts.length === 0) { return res.status(200).json({ payments: [], limit, offset, }); } const contractIds = clientContracts.map((c: any) => c.id); const { data: payments, error: paymentsError } = await adminSupabase .from("nexus_payments") .select(` *, contract:nexus_contracts(id, title, creator_id, total_amount, status), milestone:nexus_milestones(id, description, amount) `) .in("contract_id", contractIds) .order("created_at", { ascending: false }) .range(offset, offset + limit - 1); if (paymentsError) { return res.status(500).json({ error: paymentsError.message }); } return res.status(200).json({ payments: payments || [], limit, offset, }); } catch (error: any) { return res.status(500).json({ error: error?.message || "Server error" }); } }); // ─── Authentik SSO ──────────────────────────────────────────────────────── // Server-side OIDC PKCE flow against auth.aethex.tech. // Stateless signed state token — survives server restarts, no in-memory store needed. // state = base64url(payload).hmac where payload = base64url(JSON({verifier,redirectTo,exp})) const AK_STATE_SECRET = process.env.AUTHENTIK_CLIENT_SECRET || process.env.SUPABASE_SERVICE_ROLE_KEY || "fallback-secret"; const signAkState = (verifier: string, redirectTo: string): string => { const payload = Buffer.from(JSON.stringify({ v: verifier, r: redirectTo, exp: Date.now() + 10 * 60 * 1000 })).toString("base64url"); const sig = createHmac("sha256", AK_STATE_SECRET).update(payload).digest("base64url"); return `${payload}.${sig}`; }; const verifyAkState = (state: string): { verifier: string; redirectTo: string } | null => { try { const dot = state.lastIndexOf("."); if (dot === -1) return null; const payload = state.slice(0, dot); const sig = state.slice(dot + 1); const expected = createHmac("sha256", AK_STATE_SECRET).update(payload).digest("base64url"); if (sig !== expected) return null; const data = JSON.parse(Buffer.from(payload, "base64url").toString()); if (!data.exp || Date.now() > data.exp) return null; return { verifier: data.v, redirectTo: data.r || "/dashboard" }; } catch { return null; } }; app.get("/api/auth/authentik/start", (req, res) => { try { const clientId = process.env.AUTHENTIK_CLIENT_ID; const authentikBase = process.env.AUTHENTIK_BASE_URL || "https://auth.aethex.tech"; if (!clientId || clientId === "REPLACE_WITH_YOUR_AUTHENTIK_CLIENT_ID") { return res.status(500).send("Authentik SSO is not configured yet. Set AUTHENTIK_CLIENT_ID in .env"); } const baseUrl = process.env.PUBLIC_BASE_URL || process.env.SITE_URL || "https://aethex.dev"; const redirectUri = `${baseUrl}/api/auth/authentik/callback`; // PKCE const codeVerifier = randomBytes(48).toString("base64url"); const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url"); const redirectTo = (req.query.redirectTo as string) || "/dashboard"; const state = signAkState(codeVerifier, redirectTo); const params = new URLSearchParams({ client_id: clientId, response_type: "code", redirect_uri: redirectUri, scope: "openid email profile", state, code_challenge: codeChallenge, code_challenge_method: "S256", }); const authorizeUrl = `${authentikBase}/application/o/authorize/?${params.toString()}`; console.log("[Authentik] Starting OIDC flow, redirectUri:", redirectUri); return res.redirect(302, authorizeUrl); } catch (e: any) { console.error("[Authentik] start error:", e); return res.status(500).json({ error: e?.message || String(e) }); } }); app.get("/api/auth/authentik/callback", async (req, res) => { try { const code = req.query.code as string; const state = req.query.state as string; const error = req.query.error as string; const errorDesc = req.query.error_description as string; if (error) { console.error("[Authentik] callback error from provider:", error, errorDesc); return res.redirect(`/login?error=${encodeURIComponent(error)}&desc=${encodeURIComponent(errorDesc || "")}`); } if (!code || !state) { return res.redirect("/login?error=no_code"); } // Verify signed state — CSRF-safe, restart-safe const stateData = verifyAkState(state); if (!stateData) { console.error("[Authentik] invalid/expired state"); return res.redirect("/login?error=state_mismatch"); } const { verifier: codeVerifier, redirectTo } = stateData; const clientId = process.env.AUTHENTIK_CLIENT_ID!; const clientSecret = process.env.AUTHENTIK_CLIENT_SECRET!; const authentikBase = process.env.AUTHENTIK_BASE_URL || "https://auth.aethex.tech"; const providerSlug = process.env.AUTHENTIK_PROVIDER_SLUG || "aethex-forge"; const baseUrl = process.env.PUBLIC_BASE_URL || process.env.SITE_URL || "https://aethex.dev"; const redirectUri = `${baseUrl}/api/auth/authentik/callback`; // Exchange code for tokens — endpoint from discovery, no provider slug in path const tokenUrl = `${authentikBase}/application/o/token/`; const tokenBody = new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: redirectUri, client_id: clientId, client_secret: clientSecret, code_verifier: codeVerifier, }); const tokenResp = await httpsRequest(tokenUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: tokenBody.toString(), }); if (tokenResp.status < 200 || tokenResp.status >= 300) { const tokenErr = tokenResp.text(); console.error("[Authentik] token exchange failed — status:", tokenResp.status, "url:", tokenUrl, "body:", tokenErr, "redirect_uri:", redirectUri, "client_id:", clientId); return res.redirect(`/login?error=token_exchange_failed`); } const tokens = tokenResp.json() as { access_token: string; id_token?: string }; // Fetch user info const userInfoUrl = `${authentikBase}/application/o/userinfo/`; const userInfoResp = await httpsRequest(userInfoUrl, { headers: { Authorization: `Bearer ${tokens.access_token}` }, }); if (userInfoResp.status < 200 || userInfoResp.status >= 300) { console.error("[Authentik] userinfo failed:", userInfoResp.text()); return res.redirect("/login?error=userinfo_failed"); } const userInfo = userInfoResp.json() as { sub: string; email: string; email_verified?: boolean; name?: string; preferred_username?: string; given_name?: string; family_name?: string; groups?: string[]; }; if (!userInfo.email) { console.error("[Authentik] no email in userinfo:", userInfo); return res.redirect("/login?error=no_email"); } console.log("[Authentik] userinfo:", { sub: userInfo.sub, email: userInfo.email }); // Find or create Supabase user // Priority: 1) pre-linked authentik_sub in metadata, 2) email match, 3) create new const { data: existingUsers, error: lookupError } = await adminSupabase.auth.admin.listUsers({ perPage: 1000 }); if (lookupError) { console.error("[Authentik] user lookup error:", lookupError); return res.redirect("/login?error=user_lookup"); } const allUsers = existingUsers?.users || []; // Check for pre-linked sub first — this is how existing accounts get tied to Authentik let supabaseUser = allUsers.find((u: any) => u.user_metadata?.authentik_sub === userInfo.sub) // Fall back to email match ?? allUsers.find((u: any) => u.email?.toLowerCase() === userInfo.email.toLowerCase()); if (!supabaseUser) { // Create new user const { data: newUser, error: createError } = await adminSupabase.auth.admin.createUser({ email: userInfo.email, email_confirm: true, user_metadata: { full_name: userInfo.name || userInfo.preferred_username || userInfo.email.split("@")[0], authentik_sub: userInfo.sub, provider: "authentik", }, }); if (createError || !newUser?.user) { console.error("[Authentik] user create error:", createError); return res.redirect("/login?error=user_create_failed"); } supabaseUser = newUser.user; console.log("[Authentik] Created new Supabase user:", supabaseUser.id); } else { // Update metadata to note Authentik link await adminSupabase.auth.admin.updateUserById(supabaseUser.id, { user_metadata: { ...supabaseUser.user_metadata, authentik_sub: userInfo.sub, authentik_linked: true, }, }); console.log("[Authentik] Linked existing Supabase user:", supabaseUser.id); } // Generate a Supabase magic link using the MATCHED user's email, not the Authentik email. // This ensures we sign into mrpiglr@gmail.com even when Authentik says mrpiglr@aethex.dev. const { data: linkData, error: linkError } = await adminSupabase.auth.admin.generateLink({ type: "magiclink", email: supabaseUser.email!, options: { redirectTo: `${baseUrl}${redirectTo}`, }, }); if (linkError || !linkData?.properties?.action_link) { console.error("[Authentik] generateLink error:", linkError); return res.redirect("/login?error=link_gen_failed"); } // Clear flow cookies res.clearCookie("ak_state", { path: "/" }); res.clearCookie("ak_verifier", { path: "/" }); res.clearCookie("ak_redirect", { path: "/" }); console.log("[Authentik] Redirecting user to Supabase action link → dashboard"); return res.redirect(302, linkData.properties.action_link); } catch (e: any) { console.error("[Authentik] callback error:", e); return res.redirect(`/login?error=server_error`); } }); // Unlink AeThex ID — removes authentik_sub from user metadata app.post("/api/auth/authentik/unlink", async (req, res) => { try { const authHeader = req.headers.authorization || ""; const token = authHeader.replace(/^Bearer\s+/i, ""); if (!token) return res.status(401).json({ error: "Unauthorized" }); // Verify the session token and get user const { createClient } = await import("@supabase/supabase-js"); const userClient = createClient( process.env.SUPABASE_URL || process.env.VITE_SUPABASE_URL || "", process.env.SUPABASE_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY || "", ); const { data: { user }, error: authErr } = await userClient.auth.getUser(token); if (authErr || !user) return res.status(401).json({ error: "Invalid session" }); // Strip authentik fields from metadata const meta = { ...(user.user_metadata || {}) }; delete meta.authentik_sub; delete meta.authentik_linked; const { error: updateErr } = await adminSupabase.auth.admin.updateUserById(user.id, { user_metadata: meta, }); if (updateErr) return res.status(500).json({ error: updateErr.message }); console.log("[Authentik] Unlinked user:", user.id); return res.json({ ok: true }); } catch (e: any) { return res.status(500).json({ error: e?.message || String(e) }); } }); // ── End Authentik SSO ────────────────────────────────────────────────────── // Blog API routes app.get("/api/blog", blogIndexHandler); app.get("/api/blog/:slug", (req: express.Request, res: express.Response) => { return blogSlugHandler(req, res); }); // AI Chat API routes app.post("/api/ai/chat", aiChatHandler); app.post("/api/ai/title", aiTitleHandler); return app; }