mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-17 22:27:19 +00:00
- ModuleManager: Central tracking for installed marketplace modules - DataAnalyzerWidget: Real-time CPU/RAM/Battery/Storage widget (unlocked by Data Analyzer module) - BottomNavBar: Navigation bar for Projects/Chat/Marketplace/Settings - RootShell: Real root command execution utility - TerminalActivity: Full root shell with neofetch, sysinfo, real Linux commands - Terminal Pro module: Adds aliases (ll, la, h), command history - ArcadeActivity + SnakeGame: Pixel Arcade module unlocks retro games - fade_in/fade_out animations for smooth transitions
329 lines
9.7 KiB
TypeScript
329 lines
9.7 KiB
TypeScript
/**
|
|
* Discord Integration Routes
|
|
* OAuth and activity sync for Discord accounts
|
|
* Ported from aethex-forge
|
|
*/
|
|
|
|
import { Router, Request, Response } from "express";
|
|
import { supabase } from "./supabase.js";
|
|
import { requireAuth } from "./auth.js";
|
|
import crypto from "crypto";
|
|
|
|
const router = Router();
|
|
|
|
// Helper to get user ID from session
|
|
function getUserId(req: Request): string | null {
|
|
return (req.session as any)?.userId || null;
|
|
}
|
|
|
|
// Discord API base URL
|
|
const DISCORD_API = "https://discord.com/api/v10";
|
|
const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID;
|
|
const DISCORD_CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET;
|
|
const DISCORD_REDIRECT_URI = process.env.DISCORD_REDIRECT_URI || "http://localhost:5000/api/discord/oauth/callback";
|
|
|
|
// Store pending linking sessions
|
|
const linkingSessions = new Map<string, { userId: string; expiresAt: number }>();
|
|
|
|
// ==================== OAUTH ROUTES ====================
|
|
|
|
/**
|
|
* GET /api/discord/oauth/start - Start Discord OAuth flow
|
|
*/
|
|
router.get("/oauth/start", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const userId = getUserId(req);
|
|
if (!userId) return res.status(401).json({ error: "Unauthorized" });
|
|
|
|
if (!DISCORD_CLIENT_ID) {
|
|
return res.status(500).json({ error: "Discord OAuth not configured" });
|
|
}
|
|
|
|
// Create state token for CSRF protection
|
|
const state = crypto.randomUUID();
|
|
linkingSessions.set(state, {
|
|
userId,
|
|
expiresAt: Date.now() + 600000 // 10 minutes
|
|
});
|
|
|
|
const params = new URLSearchParams({
|
|
client_id: DISCORD_CLIENT_ID,
|
|
redirect_uri: DISCORD_REDIRECT_URI,
|
|
response_type: "code",
|
|
scope: "identify guilds",
|
|
state
|
|
});
|
|
|
|
res.json({
|
|
authUrl: `https://discord.com/api/oauth2/authorize?${params.toString()}`
|
|
});
|
|
} catch (err: any) {
|
|
console.error("[Discord] OAuth start error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/discord/oauth/callback - Handle Discord OAuth callback
|
|
*/
|
|
router.get("/oauth/callback", async (req: Request, res: Response) => {
|
|
try {
|
|
const { code, state } = req.query;
|
|
|
|
if (!code || !state) {
|
|
return res.status(400).json({ error: "Missing code or state" });
|
|
}
|
|
|
|
// Verify state
|
|
const session = linkingSessions.get(state as string);
|
|
if (!session || session.expiresAt < Date.now()) {
|
|
linkingSessions.delete(state as string);
|
|
return res.status(400).json({ error: "Invalid or expired state" });
|
|
}
|
|
|
|
linkingSessions.delete(state as string);
|
|
const userId = session.userId;
|
|
|
|
if (!DISCORD_CLIENT_ID || !DISCORD_CLIENT_SECRET) {
|
|
return res.status(500).json({ error: "Discord OAuth not configured" });
|
|
}
|
|
|
|
// Exchange code for token
|
|
const tokenResponse = await fetch(`${DISCORD_API}/oauth2/token`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
body: new URLSearchParams({
|
|
client_id: DISCORD_CLIENT_ID,
|
|
client_secret: DISCORD_CLIENT_SECRET,
|
|
grant_type: "authorization_code",
|
|
code: code as string,
|
|
redirect_uri: DISCORD_REDIRECT_URI
|
|
})
|
|
});
|
|
|
|
if (!tokenResponse.ok) {
|
|
throw new Error("Failed to exchange code for token");
|
|
}
|
|
|
|
const tokenData = await tokenResponse.json();
|
|
const accessToken = tokenData.access_token;
|
|
|
|
// Get Discord user info
|
|
const userResponse = await fetch(`${DISCORD_API}/users/@me`, {
|
|
headers: { Authorization: `Bearer ${accessToken}` }
|
|
});
|
|
|
|
if (!userResponse.ok) {
|
|
throw new Error("Failed to get Discord user info");
|
|
}
|
|
|
|
const discordUser = await userResponse.json();
|
|
|
|
// Store Discord link in database
|
|
await supabase
|
|
.from("user_linked_accounts")
|
|
.upsert({
|
|
user_id: userId,
|
|
provider: "discord",
|
|
provider_user_id: discordUser.id,
|
|
provider_username: discordUser.username,
|
|
provider_avatar: discordUser.avatar
|
|
? `https://cdn.discordapp.com/avatars/${discordUser.id}/${discordUser.avatar}.png`
|
|
: null,
|
|
access_token: accessToken,
|
|
refresh_token: tokenData.refresh_token,
|
|
token_expires_at: tokenData.expires_in
|
|
? new Date(Date.now() + tokenData.expires_in * 1000).toISOString()
|
|
: null,
|
|
linked_at: new Date().toISOString()
|
|
}, { onConflict: "user_id,provider" });
|
|
|
|
// Redirect to success page
|
|
res.redirect("/hub/settings?discord_linked=true");
|
|
} catch (err: any) {
|
|
console.error("[Discord] OAuth callback error:", err);
|
|
res.redirect("/hub/settings?discord_error=true");
|
|
}
|
|
});
|
|
|
|
// ==================== LINKING ROUTES ====================
|
|
|
|
/**
|
|
* POST /api/discord/link - Link Discord using verification code
|
|
* Alternative to OAuth - for Discord bots to verify users
|
|
*/
|
|
router.post("/link", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const userId = getUserId(req);
|
|
if (!userId) return res.status(401).json({ error: "Unauthorized" });
|
|
|
|
const { discord_id, verification_code } = req.body;
|
|
|
|
if (!discord_id || !verification_code) {
|
|
return res.status(400).json({ error: "discord_id and verification_code are required" });
|
|
}
|
|
|
|
// Verify the code (stored by Discord bot)
|
|
const { data: pendingLink, error: lookupError } = await supabase
|
|
.from("discord_pending_links")
|
|
.select("*")
|
|
.eq("discord_id", discord_id)
|
|
.eq("verification_code", verification_code)
|
|
.gt("expires_at", new Date().toISOString())
|
|
.single();
|
|
|
|
if (lookupError || !pendingLink) {
|
|
return res.status(400).json({ error: "Invalid or expired verification code" });
|
|
}
|
|
|
|
// Link the account
|
|
await supabase
|
|
.from("user_linked_accounts")
|
|
.upsert({
|
|
user_id: userId,
|
|
provider: "discord",
|
|
provider_user_id: discord_id,
|
|
provider_username: pendingLink.discord_username,
|
|
linked_at: new Date().toISOString()
|
|
}, { onConflict: "user_id,provider" });
|
|
|
|
// Delete the pending link
|
|
await supabase
|
|
.from("discord_pending_links")
|
|
.delete()
|
|
.eq("id", pendingLink.id);
|
|
|
|
res.json({ success: true, message: "Discord account linked successfully" });
|
|
} catch (err: any) {
|
|
console.error("[Discord] Link error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /api/discord/link - Unlink Discord account
|
|
*/
|
|
router.delete("/link", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const userId = getUserId(req);
|
|
if (!userId) return res.status(401).json({ error: "Unauthorized" });
|
|
|
|
await supabase
|
|
.from("user_linked_accounts")
|
|
.delete()
|
|
.eq("user_id", userId)
|
|
.eq("provider", "discord");
|
|
|
|
res.json({ success: true, message: "Discord account unlinked" });
|
|
} catch (err: any) {
|
|
console.error("[Discord] Unlink error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/discord/status - Check Discord link status
|
|
*/
|
|
router.get("/status", requireAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const userId = getUserId(req);
|
|
if (!userId) return res.status(401).json({ error: "Unauthorized" });
|
|
|
|
const { data, error } = await supabase
|
|
.from("user_linked_accounts")
|
|
.select("provider_user_id, provider_username, provider_avatar, linked_at")
|
|
.eq("user_id", userId)
|
|
.eq("provider", "discord")
|
|
.single();
|
|
|
|
if (error && error.code !== "PGRST116") throw error;
|
|
|
|
res.json({
|
|
linked: !!data,
|
|
discord: data ? {
|
|
id: data.provider_user_id,
|
|
username: data.provider_username,
|
|
avatar: data.provider_avatar,
|
|
linkedAt: data.linked_at
|
|
} : null
|
|
});
|
|
} catch (err: any) {
|
|
console.error("[Discord] Status error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// ==================== ACTIVITY AUTH ROUTE ====================
|
|
|
|
/**
|
|
* POST /api/discord/activity-auth - Authenticate Discord activity
|
|
* Used by Discord Activities SDK for embedded apps
|
|
*/
|
|
router.post("/activity-auth", async (req: Request, res: Response) => {
|
|
try {
|
|
const { access_token } = req.body;
|
|
|
|
if (!access_token) {
|
|
return res.status(400).json({ error: "access_token is required" });
|
|
}
|
|
|
|
// Verify token with Discord
|
|
const discordResponse = await fetch(`${DISCORD_API}/users/@me`, {
|
|
headers: { Authorization: `Bearer ${access_token}` }
|
|
});
|
|
|
|
if (!discordResponse.ok) {
|
|
if (discordResponse.status === 401) {
|
|
return res.status(401).json({ error: "Invalid or expired access token" });
|
|
}
|
|
throw new Error(`Discord API error: ${discordResponse.statusText}`);
|
|
}
|
|
|
|
const discordUser = await discordResponse.json();
|
|
const discord_id = discordUser.id;
|
|
|
|
// Find linked user account
|
|
const { data: linkedAccount } = await supabase
|
|
.from("user_linked_accounts")
|
|
.select("user_id")
|
|
.eq("provider", "discord")
|
|
.eq("provider_user_id", discord_id)
|
|
.single();
|
|
|
|
if (!linkedAccount) {
|
|
// User not linked - return Discord info for potential signup
|
|
return res.json({
|
|
linked: false,
|
|
discord: {
|
|
id: discord_id,
|
|
username: discordUser.username,
|
|
globalName: discordUser.global_name,
|
|
avatar: discordUser.avatar
|
|
? `https://cdn.discordapp.com/avatars/${discord_id}/${discordUser.avatar}.png`
|
|
: null
|
|
}
|
|
});
|
|
}
|
|
|
|
// Get user data
|
|
const { data: user } = await supabase
|
|
.from("users")
|
|
.select("id, email")
|
|
.eq("id", linkedAccount.user_id)
|
|
.single();
|
|
|
|
res.json({
|
|
linked: true,
|
|
user: user ? {
|
|
id: user.id,
|
|
email: user.email,
|
|
discord_id
|
|
} : null
|
|
});
|
|
} catch (err: any) {
|
|
console.error("[Discord] Activity auth error:", err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
export default router;
|