mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-18 06:17:21 +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
428 lines
12 KiB
TypeScript
428 lines
12 KiB
TypeScript
import { RequestHandler } from "express";
|
|
import { createClient } from "@supabase/supabase-js";
|
|
import crypto from "crypto";
|
|
|
|
const supabase = createClient(
|
|
process.env.SUPABASE_URL!,
|
|
process.env.SUPABASE_SERVICE_ROLE!
|
|
);
|
|
|
|
// Generate a secure API key
|
|
function generateApiKey(): { fullKey: string; prefix: string; hash: string } {
|
|
// Format: aethex_sk_<32 random bytes as hex>
|
|
const randomBytes = crypto.randomBytes(32).toString("hex");
|
|
const fullKey = `aethex_sk_${randomBytes}`;
|
|
const prefix = fullKey.substring(0, 16); // "aethex_sk_12345678"
|
|
const hash = crypto.createHash("sha256").update(fullKey).digest("hex");
|
|
|
|
return { fullKey, prefix, hash };
|
|
}
|
|
|
|
// Verify API key from request
|
|
export async function verifyApiKey(key: string) {
|
|
const hash = crypto.createHash("sha256").update(key).digest("hex");
|
|
|
|
const { data: apiKey, error } = await supabase
|
|
.from("api_keys")
|
|
.select("*, developer_profiles!inner(*)")
|
|
.eq("key_hash", hash)
|
|
.eq("is_active", true)
|
|
.single();
|
|
|
|
if (error || !apiKey) {
|
|
return null;
|
|
}
|
|
|
|
// Check if expired
|
|
if (apiKey.expires_at && new Date(apiKey.expires_at) < new Date()) {
|
|
return null;
|
|
}
|
|
|
|
return apiKey;
|
|
}
|
|
|
|
// GET /api/developer/keys - List all API keys for user
|
|
export const listKeys: RequestHandler = async (req, res) => {
|
|
try {
|
|
const userId = req.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: "Unauthorized" });
|
|
}
|
|
|
|
const { data: keys, error } = await supabase
|
|
.from("api_keys")
|
|
.select("id, name, key_prefix, scopes, last_used_at, usage_count, is_active, created_at, expires_at, rate_limit_per_minute, rate_limit_per_day")
|
|
.eq("user_id", userId)
|
|
.order("created_at", { ascending: false });
|
|
|
|
if (error) {
|
|
console.error("Error fetching API keys:", error);
|
|
return res.status(500).json({ error: "Failed to fetch API keys" });
|
|
}
|
|
|
|
res.json({ keys });
|
|
} catch (error) {
|
|
console.error("Error in listKeys:", error);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
};
|
|
|
|
// POST /api/developer/keys - Create new API key
|
|
export const createKey: RequestHandler = async (req, res) => {
|
|
try {
|
|
const userId = req.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: "Unauthorized" });
|
|
}
|
|
|
|
const { name, scopes = ["read"], expiresInDays } = req.body;
|
|
|
|
if (!name || name.trim().length === 0) {
|
|
return res.status(400).json({ error: "Name is required" });
|
|
}
|
|
|
|
if (name.length > 50) {
|
|
return res.status(400).json({ error: "Name must be 50 characters or less" });
|
|
}
|
|
|
|
// Check developer profile limits
|
|
const { data: profile } = await supabase
|
|
.from("developer_profiles")
|
|
.select("max_api_keys")
|
|
.eq("user_id", userId)
|
|
.single();
|
|
|
|
const maxKeys = profile?.max_api_keys || 3;
|
|
|
|
// Count existing keys
|
|
const { count } = await supabase
|
|
.from("api_keys")
|
|
.select("*", { count: "exact", head: true })
|
|
.eq("user_id", userId)
|
|
.eq("is_active", true);
|
|
|
|
if (count && count >= maxKeys) {
|
|
return res.status(403).json({
|
|
error: `Maximum of ${maxKeys} API keys reached. Delete an existing key first.`,
|
|
});
|
|
}
|
|
|
|
// Validate scopes
|
|
const validScopes = ["read", "write", "admin"];
|
|
const invalidScopes = scopes.filter((s: string) => !validScopes.includes(s));
|
|
if (invalidScopes.length > 0) {
|
|
return res.status(400).json({
|
|
error: `Invalid scopes: ${invalidScopes.join(", ")}`,
|
|
});
|
|
}
|
|
|
|
// Generate key
|
|
const { fullKey, prefix, hash } = generateApiKey();
|
|
|
|
// Calculate expiration
|
|
let expiresAt = null;
|
|
if (expiresInDays && expiresInDays > 0) {
|
|
expiresAt = new Date();
|
|
expiresAt.setDate(expiresAt.getDate() + expiresInDays);
|
|
}
|
|
|
|
// Insert into database
|
|
const { data: newKey, error } = await supabase
|
|
.from("api_keys")
|
|
.insert({
|
|
user_id: userId,
|
|
name: name.trim(),
|
|
key_prefix: prefix,
|
|
key_hash: hash,
|
|
scopes,
|
|
expires_at: expiresAt,
|
|
created_by_ip: req.ip,
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (error) {
|
|
console.error("Error creating API key:", error);
|
|
return res.status(500).json({ error: "Failed to create API key" });
|
|
}
|
|
|
|
// Return the full key ONLY on creation (never stored or shown again)
|
|
res.json({
|
|
message: "API key created successfully",
|
|
key: {
|
|
...newKey,
|
|
full_key: fullKey, // Only returned once
|
|
},
|
|
warning: "Save this key securely. It will not be shown again.",
|
|
});
|
|
} catch (error) {
|
|
console.error("Error in createKey:", error);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
};
|
|
|
|
// DELETE /api/developer/keys/:id - Delete (revoke) an API key
|
|
export const deleteKey: RequestHandler = async (req, res) => {
|
|
try {
|
|
const userId = req.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: "Unauthorized" });
|
|
}
|
|
|
|
const { id } = req.params;
|
|
|
|
// Verify ownership and delete
|
|
const { data, error } = await supabase
|
|
.from("api_keys")
|
|
.delete()
|
|
.eq("id", id)
|
|
.eq("user_id", userId)
|
|
.select()
|
|
.single();
|
|
|
|
if (error || !data) {
|
|
return res.status(404).json({ error: "API key not found" });
|
|
}
|
|
|
|
res.json({ message: "API key deleted successfully" });
|
|
} catch (error) {
|
|
console.error("Error in deleteKey:", error);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
};
|
|
|
|
// PATCH /api/developer/keys/:id - Update API key (name, scopes, active status)
|
|
export const updateKey: RequestHandler = async (req, res) => {
|
|
try {
|
|
const userId = req.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: "Unauthorized" });
|
|
}
|
|
|
|
const { id } = req.params;
|
|
const { name, scopes, is_active } = req.body;
|
|
|
|
const updates: any = {};
|
|
|
|
if (name !== undefined) {
|
|
if (name.trim().length === 0) {
|
|
return res.status(400).json({ error: "Name cannot be empty" });
|
|
}
|
|
if (name.length > 50) {
|
|
return res.status(400).json({ error: "Name must be 50 characters or less" });
|
|
}
|
|
updates.name = name.trim();
|
|
}
|
|
|
|
if (scopes !== undefined) {
|
|
const validScopes = ["read", "write", "admin"];
|
|
const invalidScopes = scopes.filter((s: string) => !validScopes.includes(s));
|
|
if (invalidScopes.length > 0) {
|
|
return res.status(400).json({
|
|
error: `Invalid scopes: ${invalidScopes.join(", ")}`,
|
|
});
|
|
}
|
|
updates.scopes = scopes;
|
|
}
|
|
|
|
if (is_active !== undefined) {
|
|
updates.is_active = Boolean(is_active);
|
|
}
|
|
|
|
if (Object.keys(updates).length === 0) {
|
|
return res.status(400).json({ error: "No updates provided" });
|
|
}
|
|
|
|
// Update
|
|
const { data, error } = await supabase
|
|
.from("api_keys")
|
|
.update(updates)
|
|
.eq("id", id)
|
|
.eq("user_id", userId)
|
|
.select()
|
|
.single();
|
|
|
|
if (error || !data) {
|
|
return res.status(404).json({ error: "API key not found" });
|
|
}
|
|
|
|
res.json({
|
|
message: "API key updated successfully",
|
|
key: data,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error in updateKey:", error);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
};
|
|
|
|
// GET /api/developer/keys/:id/stats - Get usage statistics for a key
|
|
export const getKeyStats: RequestHandler = async (req, res) => {
|
|
try {
|
|
const userId = req.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: "Unauthorized" });
|
|
}
|
|
|
|
const { id } = req.params;
|
|
|
|
// Verify ownership
|
|
const { data: key, error: keyError } = await supabase
|
|
.from("api_keys")
|
|
.select("id")
|
|
.eq("id", id)
|
|
.eq("user_id", userId)
|
|
.single();
|
|
|
|
if (keyError || !key) {
|
|
return res.status(404).json({ error: "API key not found" });
|
|
}
|
|
|
|
// Get stats using the database function
|
|
const { data: stats, error: statsError } = await supabase.rpc(
|
|
"get_api_key_stats",
|
|
{ key_id: id }
|
|
);
|
|
|
|
if (statsError) {
|
|
console.error("Error fetching key stats:", statsError);
|
|
return res.status(500).json({ error: "Failed to fetch statistics" });
|
|
}
|
|
|
|
// Get recent usage logs
|
|
const { data: recentLogs, error: logsError } = await supabase
|
|
.from("api_usage_logs")
|
|
.select("endpoint, method, status_code, timestamp, response_time_ms")
|
|
.eq("api_key_id", id)
|
|
.order("timestamp", { ascending: false })
|
|
.limit(100);
|
|
|
|
if (logsError) {
|
|
console.error("Error fetching recent logs:", logsError);
|
|
}
|
|
|
|
// Get usage by day (last 30 days)
|
|
const { data: dailyUsage, error: dailyError } = await supabase
|
|
.from("api_usage_logs")
|
|
.select("timestamp")
|
|
.eq("api_key_id", id)
|
|
.gte("timestamp", new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString())
|
|
.order("timestamp", { ascending: true });
|
|
|
|
if (dailyError) {
|
|
console.error("Error fetching daily usage:", dailyError);
|
|
}
|
|
|
|
// Group by day
|
|
const usageByDay: Record<string, number> = {};
|
|
if (dailyUsage) {
|
|
dailyUsage.forEach((log) => {
|
|
const day = new Date(log.timestamp).toISOString().split("T")[0];
|
|
usageByDay[day] = (usageByDay[day] || 0) + 1;
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
stats: stats?.[0] || {
|
|
total_requests: 0,
|
|
requests_today: 0,
|
|
requests_this_week: 0,
|
|
avg_response_time_ms: 0,
|
|
error_rate: 0,
|
|
},
|
|
recentLogs: recentLogs || [],
|
|
usageByDay,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error in getKeyStats:", error);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
};
|
|
|
|
// GET /api/developer/profile - Get developer profile
|
|
export const getProfile: RequestHandler = async (req, res) => {
|
|
try {
|
|
const userId = req.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: "Unauthorized" });
|
|
}
|
|
|
|
let { data: profile, error } = await supabase
|
|
.from("developer_profiles")
|
|
.select("*")
|
|
.eq("user_id", userId)
|
|
.single();
|
|
|
|
// Create profile if doesn't exist
|
|
if (error && error.code === "PGRST116") {
|
|
const { data: newProfile, error: createError } = await supabase
|
|
.from("developer_profiles")
|
|
.insert({ user_id: userId })
|
|
.select()
|
|
.single();
|
|
|
|
if (createError) {
|
|
console.error("Error creating developer profile:", createError);
|
|
return res.status(500).json({ error: "Failed to create profile" });
|
|
}
|
|
|
|
profile = newProfile;
|
|
} else if (error) {
|
|
console.error("Error fetching developer profile:", error);
|
|
return res.status(500).json({ error: "Failed to fetch profile" });
|
|
}
|
|
|
|
res.json({ profile });
|
|
} catch (error) {
|
|
console.error("Error in getProfile:", error);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
};
|
|
|
|
// PATCH /api/developer/profile - Update developer profile
|
|
export const updateProfile: RequestHandler = async (req, res) => {
|
|
try {
|
|
const userId = req.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ error: "Unauthorized" });
|
|
}
|
|
|
|
const { company_name, website_url, github_username } = req.body;
|
|
|
|
const updates: any = {};
|
|
|
|
if (company_name !== undefined) {
|
|
updates.company_name = company_name?.trim() || null;
|
|
}
|
|
if (website_url !== undefined) {
|
|
updates.website_url = website_url?.trim() || null;
|
|
}
|
|
if (github_username !== undefined) {
|
|
updates.github_username = github_username?.trim() || null;
|
|
}
|
|
|
|
if (Object.keys(updates).length === 0) {
|
|
return res.status(400).json({ error: "No updates provided" });
|
|
}
|
|
|
|
const { data: profile, error } = await supabase
|
|
.from("developer_profiles")
|
|
.upsert({ user_id: userId, ...updates })
|
|
.eq("user_id", userId)
|
|
.select()
|
|
.single();
|
|
|
|
if (error) {
|
|
console.error("Error updating developer profile:", error);
|
|
return res.status(500).json({ error: "Failed to update profile" });
|
|
}
|
|
|
|
res.json({
|
|
message: "Profile updated successfully",
|
|
profile,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error in updateProfile:", error);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
};
|