Prettier format pending files
This commit is contained in:
parent
ba233408ed
commit
e5aacf6773
25 changed files with 817 additions and 551 deletions
|
|
@ -64,18 +64,17 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||||
|
|
||||||
if (existingLink) {
|
if (existingLink) {
|
||||||
return res.status(409).json({
|
return res.status(409).json({
|
||||||
error: "This Discord account is already linked to another AeThex account",
|
error:
|
||||||
|
"This Discord account is already linked to another AeThex account",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the link
|
// Create the link
|
||||||
const { error: linkError } = await supabase
|
const { error: linkError } = await supabase.from("discord_links").insert({
|
||||||
.from("discord_links")
|
discord_id: verification.discord_id,
|
||||||
.insert({
|
user_id,
|
||||||
discord_id: verification.discord_id,
|
primary_arm: "labs", // Default to labs
|
||||||
user_id,
|
});
|
||||||
primary_arm: "labs", // Default to labs
|
|
||||||
});
|
|
||||||
|
|
||||||
if (linkError) {
|
if (linkError) {
|
||||||
console.error("Failed to create discord link:", linkError);
|
console.error("Failed to create discord link:", linkError);
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,10 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||||
|
|
||||||
// Verify request is from Discord bot (simple verification)
|
// Verify request is from Discord bot (simple verification)
|
||||||
const authorization = req.headers.authorization;
|
const authorization = req.headers.authorization;
|
||||||
if (!authorization || authorization !== `Bearer ${process.env.DISCORD_BOT_TOKEN}`) {
|
if (
|
||||||
|
!authorization ||
|
||||||
|
authorization !== `Bearer ${process.env.DISCORD_BOT_TOKEN}`
|
||||||
|
) {
|
||||||
return res.status(401).json({ error: "Unauthorized" });
|
return res.status(401).json({ error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,11 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||||
const secret = process.env.ROBLOX_SHARED_SECRET || "";
|
const secret = process.env.ROBLOX_SHARED_SECRET || "";
|
||||||
|
|
||||||
// Verify signature for security
|
// Verify signature for security
|
||||||
if (signature && secret && !verifyRobloxSignature(payload, signature, secret)) {
|
if (
|
||||||
|
signature &&
|
||||||
|
secret &&
|
||||||
|
!verifyRobloxSignature(payload, signature, secret)
|
||||||
|
) {
|
||||||
console.warn("Invalid Roblox signature");
|
console.warn("Invalid Roblox signature");
|
||||||
return res.status(401).json({ error: "Invalid signature" });
|
return res.status(401).json({ error: "Invalid signature" });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { session_token, game } = req.method === "POST" ? req.body : req.query;
|
const { session_token, game } =
|
||||||
|
req.method === "POST" ? req.body : req.query;
|
||||||
|
|
||||||
if (!session_token) {
|
if (!session_token) {
|
||||||
return res.status(400).json({ error: "session_token is required" });
|
return res.status(400).json({ error: "session_token is required" });
|
||||||
|
|
@ -22,7 +23,9 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||||
// Find the session
|
// Find the session
|
||||||
const { data: sessionData, error: sessionError } = await supabase
|
const { data: sessionData, error: sessionError } = await supabase
|
||||||
.from("game_sessions")
|
.from("game_sessions")
|
||||||
.select("*, user_profiles!inner(id, username, email, full_name, metadata)")
|
.select(
|
||||||
|
"*, user_profiles!inner(id, username, email, full_name, metadata)",
|
||||||
|
)
|
||||||
.eq("session_token", String(session_token))
|
.eq("session_token", String(session_token))
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
|
|
@ -38,7 +41,9 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||||
|
|
||||||
// Optional: Verify game matches if provided
|
// Optional: Verify game matches if provided
|
||||||
if (game && sessionData.game !== String(game).toLowerCase()) {
|
if (game && sessionData.game !== String(game).toLowerCase()) {
|
||||||
return res.status(403).json({ error: "Token is not valid for this game" });
|
return res
|
||||||
|
.status(403)
|
||||||
|
.json({ error: "Token is not valid for this game" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update last activity
|
// Update last activity
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||||
const clientId = process.env.ROBLOX_OAUTH_CLIENT_ID;
|
const clientId = process.env.ROBLOX_OAUTH_CLIENT_ID;
|
||||||
const clientSecret = process.env.ROBLOX_OAUTH_CLIENT_SECRET;
|
const clientSecret = process.env.ROBLOX_OAUTH_CLIENT_SECRET;
|
||||||
const redirectUri =
|
const redirectUri =
|
||||||
process.env.ROBLOX_OAUTH_REDIRECT_URI || "https://aethex.dev/roblox-callback";
|
process.env.ROBLOX_OAUTH_REDIRECT_URI ||
|
||||||
|
"https://aethex.dev/roblox-callback";
|
||||||
|
|
||||||
if (!clientId || !clientSecret) {
|
if (!clientId || !clientSecret) {
|
||||||
return res.status(500).json({ error: "Roblox OAuth not configured" });
|
return res.status(500).json({ error: "Roblox OAuth not configured" });
|
||||||
|
|
@ -59,9 +60,12 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||||
const tokenData: RobloxTokenResponse = await tokenResponse.json();
|
const tokenData: RobloxTokenResponse = await tokenResponse.json();
|
||||||
|
|
||||||
// Get user info with access token
|
// Get user info with access token
|
||||||
const userResponse = await fetch("https://apis.roblox.com/oauth/v1/userinfo", {
|
const userResponse = await fetch(
|
||||||
headers: { Authorization: `Bearer ${tokenData.access_token}` },
|
"https://apis.roblox.com/oauth/v1/userinfo",
|
||||||
});
|
{
|
||||||
|
headers: { Authorization: `Bearer ${tokenData.access_token}` },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (!userResponse.ok) {
|
if (!userResponse.ok) {
|
||||||
console.error("Failed to fetch Roblox user info");
|
console.error("Failed to fetch Roblox user info");
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||||
const token = authHeader.substring(7);
|
const token = authHeader.substring(7);
|
||||||
|
|
||||||
// Verify token with Supabase
|
// Verify token with Supabase
|
||||||
const { data: userData, error: authError } = await supabase.auth.getUser(token);
|
const { data: userData, error: authError } =
|
||||||
|
await supabase.auth.getUser(token);
|
||||||
if (authError || !userData.user) {
|
if (authError || !userData.user) {
|
||||||
return res.status(401).json({ error: "Invalid token" });
|
return res.status(401).json({ error: "Invalid token" });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||||
const token = authHeader.substring(7);
|
const token = authHeader.substring(7);
|
||||||
|
|
||||||
// Verify token with Supabase
|
// Verify token with Supabase
|
||||||
const { data: userData, error: authError } = await supabase.auth.getUser(token);
|
const { data: userData, error: authError } =
|
||||||
|
await supabase.auth.getUser(token);
|
||||||
if (authError || !userData.user) {
|
if (authError || !userData.user) {
|
||||||
return res.status(401).json({ error: "Invalid token" });
|
return res.status(401).json({ error: "Invalid token" });
|
||||||
}
|
}
|
||||||
|
|
@ -66,15 +67,16 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||||
.eq("nonce", nonce);
|
.eq("nonce", nonce);
|
||||||
|
|
||||||
// Link wallet to existing user
|
// Link wallet to existing user
|
||||||
const { error: linkError } = await supabase
|
const { error: linkError } = await supabase.from("web3_wallets").insert({
|
||||||
.from("web3_wallets")
|
user_id: userData.user.id,
|
||||||
.insert({
|
wallet_address: normalizedAddress,
|
||||||
user_id: userData.user.id,
|
chain_id: 1, // Ethereum mainnet
|
||||||
wallet_address: normalizedAddress,
|
});
|
||||||
chain_id: 1, // Ethereum mainnet
|
|
||||||
});
|
|
||||||
|
|
||||||
if (linkError && !linkError.message.includes("violates unique constraint")) {
|
if (
|
||||||
|
linkError &&
|
||||||
|
!linkError.message.includes("violates unique constraint")
|
||||||
|
) {
|
||||||
console.error("Failed to link wallet:", linkError);
|
console.error("Failed to link wallet:", linkError);
|
||||||
return res.status(500).json({ error: "Failed to link wallet" });
|
return res.status(500).json({ error: "Failed to link wallet" });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,15 +80,16 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||||
const username = normalizedAddress.substring(2, 10);
|
const username = normalizedAddress.substring(2, 10);
|
||||||
|
|
||||||
// Create Supabase auth user
|
// Create Supabase auth user
|
||||||
const { data: authData, error: authError } = await supabase.auth.admin.createUser({
|
const { data: authData, error: authError } =
|
||||||
email,
|
await supabase.auth.admin.createUser({
|
||||||
password: require("crypto").randomBytes(32).toString("hex"),
|
email,
|
||||||
email_confirm: true,
|
password: require("crypto").randomBytes(32).toString("hex"),
|
||||||
user_metadata: {
|
email_confirm: true,
|
||||||
wallet_address: normalizedAddress,
|
user_metadata: {
|
||||||
auth_method: "web3",
|
wallet_address: normalizedAddress,
|
||||||
},
|
auth_method: "web3",
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (authError || !authData.user) {
|
if (authError || !authData.user) {
|
||||||
console.error("Failed to create auth user:", authError);
|
console.error("Failed to create auth user:", authError);
|
||||||
|
|
|
||||||
455
client/App.tsx
455
client/App.tsx
|
|
@ -119,232 +119,247 @@ const App = () => (
|
||||||
<Web3Provider>
|
<Web3Provider>
|
||||||
<DiscordProvider>
|
<DiscordProvider>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<Analytics />
|
<Analytics />
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<SkipAgentController />
|
<SkipAgentController />
|
||||||
<PageTransition>
|
<PageTransition>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Index />} />
|
<Route path="/" element={<Index />} />
|
||||||
<Route path="/onboarding" element={<Onboarding />} />
|
<Route path="/onboarding" element={<Onboarding />} />
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
<Route path="/realms" element={<Realms />} />
|
<Route path="/realms" element={<Realms />} />
|
||||||
<Route path="/investors" element={<Investors />} />
|
<Route path="/investors" element={<Investors />} />
|
||||||
<Route path="/roadmap" element={<Roadmap />} />
|
<Route path="/roadmap" element={<Roadmap />} />
|
||||||
<Route path="/trust" element={<Trust />} />
|
<Route path="/trust" element={<Trust />} />
|
||||||
<Route path="/press" element={<PressKit />} />
|
<Route path="/press" element={<PressKit />} />
|
||||||
<Route path="/projects" element={<Projects />} />
|
<Route path="/projects" element={<Projects />} />
|
||||||
<Route path="/projects/admin" element={<ProjectsAdmin />} />
|
<Route path="/projects/admin" element={<ProjectsAdmin />} />
|
||||||
<Route path="/directory" element={<Directory />} />
|
<Route path="/directory" element={<Directory />} />
|
||||||
<Route path="/admin" element={<Admin />} />
|
<Route path="/admin" element={<Admin />} />
|
||||||
<Route path="/admin/docs-sync" element={<DocsSync />} />
|
<Route path="/admin/docs-sync" element={<DocsSync />} />
|
||||||
<Route path="/feed" element={<Feed />} />
|
<Route path="/feed" element={<Feed />} />
|
||||||
<Route path="/teams" element={<Teams />} />
|
<Route path="/teams" element={<Teams />} />
|
||||||
<Route path="/squads" element={<Squads />} />
|
<Route path="/squads" element={<Squads />} />
|
||||||
<Route path="/mentee-hub" element={<MenteeHub />} />
|
<Route path="/mentee-hub" element={<MenteeHub />} />
|
||||||
<Route path="/projects/new" element={<ProjectsNew />} />
|
<Route path="/projects/new" element={<ProjectsNew />} />
|
||||||
<Route
|
|
||||||
path="/projects/:projectId/board"
|
|
||||||
element={<ProjectBoard />}
|
|
||||||
/>
|
|
||||||
<Route path="/profile" element={<Profile />} />
|
|
||||||
<Route path="/profile/me" element={<Profile />} />
|
|
||||||
<Route
|
|
||||||
path="/profile/applications"
|
|
||||||
element={<MyApplications />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route path="/developers" element={<DevelopersDirectory />} />
|
|
||||||
<Route
|
|
||||||
path="/developers/me"
|
|
||||||
element={<LegacyPassportRedirect />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/developers/:id"
|
|
||||||
element={<LegacyPassportRedirect />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/profiles"
|
|
||||||
element={<Navigate to="/developers" replace />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/profiles/me"
|
|
||||||
element={<LegacyPassportRedirect />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/profiles/:id"
|
|
||||||
element={<LegacyPassportRedirect />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/passport"
|
|
||||||
element={<Navigate to="/passport/me" replace />}
|
|
||||||
/>
|
|
||||||
<Route path="/passport/me" element={<ProfilePassport />} />
|
|
||||||
<Route
|
|
||||||
path="/passport/:username"
|
|
||||||
element={<ProfilePassport />}
|
|
||||||
/>
|
|
||||||
<Route path="/login" element={<Login />} />
|
|
||||||
<Route path="/signup" element={<SignupRedirect />} />
|
|
||||||
<Route path="/reset-password" element={<ResetPassword />} />
|
|
||||||
<Route path="/roblox-callback" element={<RobloxCallback />} />
|
|
||||||
<Route path="/web3-callback" element={<Web3Callback />} />
|
|
||||||
<Route path="/discord-verify" element={<DiscordVerify />} />
|
|
||||||
|
|
||||||
{/* Creator Network routes */}
|
|
||||||
<Route path="/creators" element={<CreatorDirectory />} />
|
|
||||||
<Route
|
|
||||||
path="/creators/:username"
|
|
||||||
element={<CreatorProfile />}
|
|
||||||
/>
|
|
||||||
<Route path="/opportunities" element={<OpportunitiesHub />} />
|
|
||||||
<Route
|
|
||||||
path="/opportunities/:id"
|
|
||||||
element={<OpportunityDetail />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Service routes */}
|
|
||||||
<Route path="/game-development" element={<GameDevelopment />} />
|
|
||||||
<Route path="/consulting" element={<DevelopmentConsulting />} />
|
|
||||||
<Route path="/mentorship" element={<MentorshipPrograms />} />
|
|
||||||
<Route path="/engage" element={<Engage />} />
|
|
||||||
<Route
|
|
||||||
path="/pricing"
|
|
||||||
element={<Navigate to="/engage" replace />}
|
|
||||||
/>
|
|
||||||
<Route path="/research" element={<ResearchLabs />} />
|
|
||||||
|
|
||||||
{/* New Arm Landing Pages */}
|
|
||||||
<Route path="/labs" element={<Labs />} />
|
|
||||||
<Route
|
|
||||||
path="/labs/explore-research"
|
|
||||||
element={<LabsExploreResearch />}
|
|
||||||
/>
|
|
||||||
<Route path="/labs/join-team" element={<LabsJoinTeam />} />
|
|
||||||
<Route
|
|
||||||
path="/labs/get-involved"
|
|
||||||
element={<LabsGetInvolved />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route path="/gameforge" element={<GameForge />} />
|
|
||||||
<Route
|
|
||||||
path="/gameforge/start-building"
|
|
||||||
element={<GameForgeStartBuilding />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/gameforge/view-portfolio"
|
|
||||||
element={<GameForgeViewPortfolio />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/gameforge/join-gameforge"
|
|
||||||
element={<GameForgeJoinGameForge />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route path="/corp" element={<Corp />} />
|
|
||||||
<Route
|
|
||||||
path="/corp/schedule-consultation"
|
|
||||||
element={<CorpScheduleConsultation />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/corp/view-case-studies"
|
|
||||||
element={<CorpViewCaseStudies />}
|
|
||||||
/>
|
|
||||||
<Route path="/corp/contact-us" element={<CorpContactUs />} />
|
|
||||||
|
|
||||||
<Route path="/foundation" element={<Foundation />} />
|
|
||||||
<Route
|
|
||||||
path="/foundation/contribute"
|
|
||||||
element={<FoundationContribute />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/foundation/learn-more"
|
|
||||||
element={<FoundationLearnMore />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/foundation/get-involved"
|
|
||||||
element={<FoundationGetInvolved />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Dev-Link routes */}
|
|
||||||
<Route path="/dev-link" element={<DevLink />} />
|
|
||||||
<Route
|
|
||||||
path="/dev-link/waitlist"
|
|
||||||
element={<DevLinkProfiles />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Nexus routes */}
|
|
||||||
<Route path="/nexus" element={<Nexus />} />
|
|
||||||
|
|
||||||
{/* Resource routes */}
|
|
||||||
<Route path="/docs" element={<DocsLayout />}>
|
|
||||||
<Route index element={<DocsOverview />} />
|
|
||||||
<Route path="tutorials" element={<DocsTutorials />} />
|
|
||||||
<Route path="curriculum" element={<DocsCurriculum />} />
|
|
||||||
<Route
|
<Route
|
||||||
path="getting-started"
|
path="/projects/:projectId/board"
|
||||||
element={<DocsGettingStarted />}
|
element={<ProjectBoard />}
|
||||||
|
/>
|
||||||
|
<Route path="/profile" element={<Profile />} />
|
||||||
|
<Route path="/profile/me" element={<Profile />} />
|
||||||
|
<Route
|
||||||
|
path="/profile/applications"
|
||||||
|
element={<MyApplications />}
|
||||||
/>
|
/>
|
||||||
<Route path="platform" element={<DocsPlatform />} />
|
|
||||||
<Route path="api" element={<DocsApiReference />} />
|
|
||||||
<Route path="cli" element={<DocsCli />} />
|
|
||||||
<Route path="examples" element={<DocsExamples />} />
|
|
||||||
<Route path="integrations" element={<DocsIntegrations />} />
|
|
||||||
</Route>
|
|
||||||
<Route path="/tutorials" element={<Tutorials />} />
|
|
||||||
<Route path="/blog" element={<Blog />} />
|
|
||||||
<Route path="/blog/:slug" element={<BlogPost />} />
|
|
||||||
<Route path="/community" element={<Community />} />
|
|
||||||
<Route path="/community/teams" element={<FoundationTeams />} />
|
|
||||||
<Route path="/community/about" element={<FoundationAbout />} />
|
|
||||||
<Route
|
|
||||||
path="/community/mentorship"
|
|
||||||
element={<MentorshipRequest />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/community/mentorship/apply"
|
|
||||||
element={<MentorApply />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/community/mentor/:username"
|
|
||||||
element={<MentorProfile />}
|
|
||||||
/>
|
|
||||||
<Route path="/community/:tabId" element={<Community />} />
|
|
||||||
<Route path="/staff" element={<Staff />} />
|
|
||||||
<Route path="/support" element={<Support />} />
|
|
||||||
<Route path="/status" element={<Status />} />
|
|
||||||
<Route path="/changelog" element={<Changelog />} />
|
|
||||||
|
|
||||||
{/* Informational routes */}
|
<Route path="/developers" element={<DevelopersDirectory />} />
|
||||||
<Route path="/wix" element={<Wix />} />
|
<Route
|
||||||
<Route path="/wix/case-studies" element={<WixCaseStudies />} />
|
path="/developers/me"
|
||||||
<Route path="/wix/faq" element={<WixFaq />} />
|
element={<LegacyPassportRedirect />}
|
||||||
<Route path="/about" element={<About />} />
|
/>
|
||||||
<Route path="/contact" element={<Contact />} />
|
<Route
|
||||||
<Route path="/get-started" element={<GetStarted />} />
|
path="/developers/:id"
|
||||||
<Route path="/explore" element={<Explore />} />
|
element={<LegacyPassportRedirect />}
|
||||||
<Route path="/services" element={<Services />} />
|
/>
|
||||||
<Route path="/careers" element={<Careers />} />
|
<Route
|
||||||
|
path="/profiles"
|
||||||
|
element={<Navigate to="/developers" replace />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/profiles/me"
|
||||||
|
element={<LegacyPassportRedirect />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/profiles/:id"
|
||||||
|
element={<LegacyPassportRedirect />}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Legal routes */}
|
<Route
|
||||||
<Route path="/privacy" element={<Privacy />} />
|
path="/passport"
|
||||||
<Route path="/terms" element={<Terms />} />
|
element={<Navigate to="/passport/me" replace />}
|
||||||
|
/>
|
||||||
|
<Route path="/passport/me" element={<ProfilePassport />} />
|
||||||
|
<Route
|
||||||
|
path="/passport/:username"
|
||||||
|
element={<ProfilePassport />}
|
||||||
|
/>
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path="/signup" element={<SignupRedirect />} />
|
||||||
|
<Route path="/reset-password" element={<ResetPassword />} />
|
||||||
|
<Route path="/roblox-callback" element={<RobloxCallback />} />
|
||||||
|
<Route path="/web3-callback" element={<Web3Callback />} />
|
||||||
|
<Route path="/discord-verify" element={<DiscordVerify />} />
|
||||||
|
|
||||||
{/* Discord routes */}
|
{/* Creator Network routes */}
|
||||||
<Route path="/discord" element={<DiscordActivity />} />
|
<Route path="/creators" element={<CreatorDirectory />} />
|
||||||
<Route
|
<Route
|
||||||
path="/discord/callback"
|
path="/creators/:username"
|
||||||
element={<DiscordOAuthCallback />}
|
element={<CreatorProfile />}
|
||||||
/>
|
/>
|
||||||
|
<Route path="/opportunities" element={<OpportunitiesHub />} />
|
||||||
|
<Route
|
||||||
|
path="/opportunities/:id"
|
||||||
|
element={<OpportunityDetail />}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Explicit 404 route for static hosting fallbacks */}
|
{/* Service routes */}
|
||||||
<Route path="/404" element={<FourOhFourPage />} />
|
<Route
|
||||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
path="/game-development"
|
||||||
<Route path="*" element={<FourOhFourPage />} />
|
element={<GameDevelopment />}
|
||||||
</Routes>
|
/>
|
||||||
</PageTransition>
|
<Route
|
||||||
</BrowserRouter>
|
path="/consulting"
|
||||||
</TooltipProvider>
|
element={<DevelopmentConsulting />}
|
||||||
|
/>
|
||||||
|
<Route path="/mentorship" element={<MentorshipPrograms />} />
|
||||||
|
<Route path="/engage" element={<Engage />} />
|
||||||
|
<Route
|
||||||
|
path="/pricing"
|
||||||
|
element={<Navigate to="/engage" replace />}
|
||||||
|
/>
|
||||||
|
<Route path="/research" element={<ResearchLabs />} />
|
||||||
|
|
||||||
|
{/* New Arm Landing Pages */}
|
||||||
|
<Route path="/labs" element={<Labs />} />
|
||||||
|
<Route
|
||||||
|
path="/labs/explore-research"
|
||||||
|
element={<LabsExploreResearch />}
|
||||||
|
/>
|
||||||
|
<Route path="/labs/join-team" element={<LabsJoinTeam />} />
|
||||||
|
<Route
|
||||||
|
path="/labs/get-involved"
|
||||||
|
element={<LabsGetInvolved />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route path="/gameforge" element={<GameForge />} />
|
||||||
|
<Route
|
||||||
|
path="/gameforge/start-building"
|
||||||
|
element={<GameForgeStartBuilding />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/gameforge/view-portfolio"
|
||||||
|
element={<GameForgeViewPortfolio />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/gameforge/join-gameforge"
|
||||||
|
element={<GameForgeJoinGameForge />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route path="/corp" element={<Corp />} />
|
||||||
|
<Route
|
||||||
|
path="/corp/schedule-consultation"
|
||||||
|
element={<CorpScheduleConsultation />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/corp/view-case-studies"
|
||||||
|
element={<CorpViewCaseStudies />}
|
||||||
|
/>
|
||||||
|
<Route path="/corp/contact-us" element={<CorpContactUs />} />
|
||||||
|
|
||||||
|
<Route path="/foundation" element={<Foundation />} />
|
||||||
|
<Route
|
||||||
|
path="/foundation/contribute"
|
||||||
|
element={<FoundationContribute />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/foundation/learn-more"
|
||||||
|
element={<FoundationLearnMore />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/foundation/get-involved"
|
||||||
|
element={<FoundationGetInvolved />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Dev-Link routes */}
|
||||||
|
<Route path="/dev-link" element={<DevLink />} />
|
||||||
|
<Route
|
||||||
|
path="/dev-link/waitlist"
|
||||||
|
element={<DevLinkProfiles />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Nexus routes */}
|
||||||
|
<Route path="/nexus" element={<Nexus />} />
|
||||||
|
|
||||||
|
{/* Resource routes */}
|
||||||
|
<Route path="/docs" element={<DocsLayout />}>
|
||||||
|
<Route index element={<DocsOverview />} />
|
||||||
|
<Route path="tutorials" element={<DocsTutorials />} />
|
||||||
|
<Route path="curriculum" element={<DocsCurriculum />} />
|
||||||
|
<Route
|
||||||
|
path="getting-started"
|
||||||
|
element={<DocsGettingStarted />}
|
||||||
|
/>
|
||||||
|
<Route path="platform" element={<DocsPlatform />} />
|
||||||
|
<Route path="api" element={<DocsApiReference />} />
|
||||||
|
<Route path="cli" element={<DocsCli />} />
|
||||||
|
<Route path="examples" element={<DocsExamples />} />
|
||||||
|
<Route path="integrations" element={<DocsIntegrations />} />
|
||||||
|
</Route>
|
||||||
|
<Route path="/tutorials" element={<Tutorials />} />
|
||||||
|
<Route path="/blog" element={<Blog />} />
|
||||||
|
<Route path="/blog/:slug" element={<BlogPost />} />
|
||||||
|
<Route path="/community" element={<Community />} />
|
||||||
|
<Route
|
||||||
|
path="/community/teams"
|
||||||
|
element={<FoundationTeams />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/community/about"
|
||||||
|
element={<FoundationAbout />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/community/mentorship"
|
||||||
|
element={<MentorshipRequest />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/community/mentorship/apply"
|
||||||
|
element={<MentorApply />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/community/mentor/:username"
|
||||||
|
element={<MentorProfile />}
|
||||||
|
/>
|
||||||
|
<Route path="/community/:tabId" element={<Community />} />
|
||||||
|
<Route path="/staff" element={<Staff />} />
|
||||||
|
<Route path="/support" element={<Support />} />
|
||||||
|
<Route path="/status" element={<Status />} />
|
||||||
|
<Route path="/changelog" element={<Changelog />} />
|
||||||
|
|
||||||
|
{/* Informational routes */}
|
||||||
|
<Route path="/wix" element={<Wix />} />
|
||||||
|
<Route
|
||||||
|
path="/wix/case-studies"
|
||||||
|
element={<WixCaseStudies />}
|
||||||
|
/>
|
||||||
|
<Route path="/wix/faq" element={<WixFaq />} />
|
||||||
|
<Route path="/about" element={<About />} />
|
||||||
|
<Route path="/contact" element={<Contact />} />
|
||||||
|
<Route path="/get-started" element={<GetStarted />} />
|
||||||
|
<Route path="/explore" element={<Explore />} />
|
||||||
|
<Route path="/services" element={<Services />} />
|
||||||
|
<Route path="/careers" element={<Careers />} />
|
||||||
|
|
||||||
|
{/* Legal routes */}
|
||||||
|
<Route path="/privacy" element={<Privacy />} />
|
||||||
|
<Route path="/terms" element={<Terms />} />
|
||||||
|
|
||||||
|
{/* Discord routes */}
|
||||||
|
<Route path="/discord" element={<DiscordActivity />} />
|
||||||
|
<Route
|
||||||
|
path="/discord/callback"
|
||||||
|
element={<DiscordOAuthCallback />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Explicit 404 route for static hosting fallbacks */}
|
||||||
|
<Route path="/404" element={<FourOhFourPage />} />
|
||||||
|
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||||
|
<Route path="*" element={<FourOhFourPage />} />
|
||||||
|
</Routes>
|
||||||
|
</PageTransition>
|
||||||
|
</BrowserRouter>
|
||||||
|
</TooltipProvider>
|
||||||
</DiscordProvider>
|
</DiscordProvider>
|
||||||
</Web3Provider>
|
</Web3Provider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { CheckCircle2 } from "lucide-react";
|
import { CheckCircle2 } from "lucide-react";
|
||||||
|
|
||||||
|
|
@ -12,7 +18,8 @@ const REALMS = [
|
||||||
{
|
{
|
||||||
id: "labs",
|
id: "labs",
|
||||||
title: "🧪 Labs",
|
title: "🧪 Labs",
|
||||||
description: "Research & Development - Cutting-edge innovation and experimentation",
|
description:
|
||||||
|
"Research & Development - Cutting-edge innovation and experimentation",
|
||||||
color: "from-yellow-500/20 to-yellow-600/20",
|
color: "from-yellow-500/20 to-yellow-600/20",
|
||||||
borderColor: "border-yellow-400",
|
borderColor: "border-yellow-400",
|
||||||
},
|
},
|
||||||
|
|
@ -46,18 +53,25 @@ const REALMS = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function RealmSelection({ selectedRealm, onSelect, onNext }: RealmSelectionProps) {
|
export default function RealmSelection({
|
||||||
|
selectedRealm,
|
||||||
|
onSelect,
|
||||||
|
onNext,
|
||||||
|
}: RealmSelectionProps) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-white mb-2">Choose Your Primary Realm</h2>
|
<h2 className="text-2xl font-bold text-white mb-2">
|
||||||
|
Choose Your Primary Realm
|
||||||
|
</h2>
|
||||||
<p className="text-gray-400">
|
<p className="text-gray-400">
|
||||||
Select the AeThex realm that best matches your primary focus. You can always change this later.
|
Select the AeThex realm that best matches your primary focus. You can
|
||||||
|
always change this later.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{REALMS.map(realm => (
|
{REALMS.map((realm) => (
|
||||||
<div
|
<div
|
||||||
key={realm.id}
|
key={realm.id}
|
||||||
onClick={() => onSelect(realm.id)}
|
onClick={() => onSelect(realm.id)}
|
||||||
|
|
@ -67,7 +81,9 @@ export default function RealmSelection({ selectedRealm, onSelect, onNext }: Real
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
className={`h-full border-2 ${realm.borderColor} ${
|
className={`h-full border-2 ${realm.borderColor} ${
|
||||||
selectedRealm === realm.id ? "ring-2 ring-offset-2 ring-white" : ""
|
selectedRealm === realm.id
|
||||||
|
? "ring-2 ring-offset-2 ring-white"
|
||||||
|
: ""
|
||||||
} hover:shadow-lg transition-shadow`}
|
} hover:shadow-lg transition-shadow`}
|
||||||
>
|
>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|
@ -81,7 +97,9 @@ export default function RealmSelection({ selectedRealm, onSelect, onNext }: Real
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<CardDescription className="text-sm">{realm.description}</CardDescription>
|
<CardDescription className="text-sm">
|
||||||
|
{realm.description}
|
||||||
|
</CardDescription>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -836,8 +836,14 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
window.localStorage.removeItem("onboarding_complete");
|
window.localStorage.removeItem("onboarding_complete");
|
||||||
window.localStorage.removeItem("aethex_onboarding_progress_v1");
|
window.localStorage.removeItem("aethex_onboarding_progress_v1");
|
||||||
Object.keys(window.localStorage)
|
Object.keys(window.localStorage)
|
||||||
.filter(key => key.startsWith("sb-") || key.includes("supabase") || key.startsWith("mock_") || key.startsWith("demo_"))
|
.filter(
|
||||||
.forEach(key => window.localStorage.removeItem(key));
|
(key) =>
|
||||||
|
key.startsWith("sb-") ||
|
||||||
|
key.includes("supabase") ||
|
||||||
|
key.startsWith("mock_") ||
|
||||||
|
key.startsWith("demo_"),
|
||||||
|
)
|
||||||
|
.forEach((key) => window.localStorage.removeItem(key));
|
||||||
console.log("localStorage cleared");
|
console.log("localStorage cleared");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("localStorage clear failed:", e);
|
console.warn("localStorage clear failed:", e);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
import React, { createContext, useContext, useState, useCallback, useEffect } from "react";
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
} from "react";
|
||||||
import { aethexToast } from "@/lib/aethex-toast";
|
import { aethexToast } from "@/lib/aethex-toast";
|
||||||
|
|
||||||
export interface Web3ContextType {
|
export interface Web3ContextType {
|
||||||
|
|
@ -13,7 +19,9 @@ export interface Web3ContextType {
|
||||||
|
|
||||||
const Web3Context = createContext<Web3ContextType | undefined>(undefined);
|
const Web3Context = createContext<Web3ContextType | undefined>(undefined);
|
||||||
|
|
||||||
export const Web3Provider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const Web3Provider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
const [account, setAccount] = useState<string | null>(null);
|
const [account, setAccount] = useState<string | null>(null);
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
|
|
@ -100,7 +108,11 @@ export const Web3Provider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||||
|
|
||||||
const signMessage = useCallback(
|
const signMessage = useCallback(
|
||||||
async (message: string): Promise<string> => {
|
async (message: string): Promise<string> => {
|
||||||
if (!account || typeof window === "undefined" || !(window as any).ethereum) {
|
if (
|
||||||
|
!account ||
|
||||||
|
typeof window === "undefined" ||
|
||||||
|
!(window as any).ethereum
|
||||||
|
) {
|
||||||
throw new Error("Wallet not connected");
|
throw new Error("Wallet not connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,8 +156,14 @@ export const Web3Provider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||||
(window as any).ethereum.on("chainChanged", handleChainChanged);
|
(window as any).ethereum.on("chainChanged", handleChainChanged);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
(window as any).ethereum?.removeListener("accountsChanged", handleAccountsChanged);
|
(window as any).ethereum?.removeListener(
|
||||||
(window as any).ethereum?.removeListener("chainChanged", handleChainChanged);
|
"accountsChanged",
|
||||||
|
handleAccountsChanged,
|
||||||
|
);
|
||||||
|
(window as any).ethereum?.removeListener(
|
||||||
|
"chainChanged",
|
||||||
|
handleChainChanged,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1513,27 +1513,59 @@ export default function Admin() {
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="bg-background/40 rounded-lg p-4 border border-border/40">
|
<div className="bg-background/40 rounded-lg p-4 border border-border/40">
|
||||||
<h3 className="font-semibold mb-4">Realm to Discord Role Mappings</h3>
|
<h3 className="font-semibold mb-4">
|
||||||
|
Realm to Discord Role Mappings
|
||||||
|
</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm text-gray-400">
|
<p className="text-sm text-gray-400">
|
||||||
Configure which Discord roles are assigned for each AeThex realm and user type.
|
Configure which Discord roles are assigned for each
|
||||||
|
AeThex realm and user type.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4 space-y-3">
|
<div className="mt-4 space-y-3">
|
||||||
{[
|
{[
|
||||||
{ realm: "Labs", role: "Labs Creator", members: 234 },
|
{
|
||||||
{ realm: "GameForge", role: "GameForge Creator", members: 456 },
|
realm: "Labs",
|
||||||
{ realm: "Corp", role: "Corp Member", members: 89 },
|
role: "Labs Creator",
|
||||||
{ realm: "Foundation", role: "Foundation Member", members: 145 },
|
members: 234,
|
||||||
{ realm: "Dev-Link", role: "Dev-Link Member", members: 78 },
|
},
|
||||||
|
{
|
||||||
|
realm: "GameForge",
|
||||||
|
role: "GameForge Creator",
|
||||||
|
members: 456,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
realm: "Corp",
|
||||||
|
role: "Corp Member",
|
||||||
|
members: 89,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
realm: "Foundation",
|
||||||
|
role: "Foundation Member",
|
||||||
|
members: 145,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
realm: "Dev-Link",
|
||||||
|
role: "Dev-Link Member",
|
||||||
|
members: 78,
|
||||||
|
},
|
||||||
].map((mapping, idx) => (
|
].map((mapping, idx) => (
|
||||||
<div key={idx} className="flex items-center justify-between p-3 bg-background/30 rounded border border-border/20">
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="flex items-center justify-between p-3 bg-background/30 rounded border border-border/20"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">{mapping.realm}</p>
|
<p className="font-medium">{mapping.realm}</p>
|
||||||
<p className="text-sm text-gray-400">{mapping.role}</p>
|
<p className="text-sm text-gray-400">
|
||||||
|
{mapping.role}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-lg font-bold text-purple-400">{mapping.members}</p>
|
<p className="text-lg font-bold text-purple-400">
|
||||||
<p className="text-xs text-gray-500">assigned members</p>
|
{mapping.members}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
assigned members
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -1542,19 +1574,31 @@ export default function Admin() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-background/40 rounded-lg p-4 border border-border/40">
|
<div className="bg-background/40 rounded-lg p-4 border border-border/40">
|
||||||
<h3 className="font-semibold mb-3">Bot Configuration</h3>
|
<h3 className="font-semibold mb-3">
|
||||||
|
Bot Configuration
|
||||||
|
</h3>
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-gray-400">Bot Status</span>
|
<span className="text-gray-400">Bot Status</span>
|
||||||
<Badge variant="default" className="bg-green-500">Online</Badge>
|
<Badge variant="default" className="bg-green-500">
|
||||||
|
Online
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-gray-400">Servers Connected</span>
|
<span className="text-gray-400">
|
||||||
<span className="font-mono text-purple-400">12 servers</span>
|
Servers Connected
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-purple-400">
|
||||||
|
12 servers
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-gray-400">Linked Accounts</span>
|
<span className="text-gray-400">
|
||||||
<span className="font-mono text-purple-400">1,234 users</span>
|
Linked Accounts
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-purple-400">
|
||||||
|
1,234 users
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,8 @@ export default function DiscordVerify() {
|
||||||
|
|
||||||
toastSuccess({
|
toastSuccess({
|
||||||
title: "Discord Linked!",
|
title: "Discord Linked!",
|
||||||
description: "Your Discord account has been successfully linked to AeThex",
|
description:
|
||||||
|
"Your Discord account has been successfully linked to AeThex",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Redirect to profile settings
|
// Redirect to profile settings
|
||||||
|
|
|
||||||
|
|
@ -514,7 +514,10 @@ export default function Onboarding() {
|
||||||
selectedRealm={data.creatorProfile.primaryArm || ""}
|
selectedRealm={data.creatorProfile.primaryArm || ""}
|
||||||
onSelect={(realm) =>
|
onSelect={(realm) =>
|
||||||
updateData({
|
updateData({
|
||||||
creatorProfile: { ...data.creatorProfile, primaryArm: realm },
|
creatorProfile: {
|
||||||
|
...data.creatorProfile,
|
||||||
|
primaryArm: realm,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onNext={nextStep}
|
onNext={nextStep}
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,8 @@ export default function RobloxCallback() {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
toastError({
|
toastError({
|
||||||
title: "Authentication failed",
|
title: "Authentication failed",
|
||||||
description: errorData.error || "Could not authenticate with Roblox",
|
description:
|
||||||
|
errorData.error || "Could not authenticate with Roblox",
|
||||||
});
|
});
|
||||||
navigate("/login");
|
navigate("/login");
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,8 @@ export default function Web3Callback() {
|
||||||
if (user && data.success) {
|
if (user && data.success) {
|
||||||
toastSuccess({
|
toastSuccess({
|
||||||
title: "Wallet linked",
|
title: "Wallet linked",
|
||||||
description: "Your Ethereum wallet is now connected to your account",
|
description:
|
||||||
|
"Your Ethereum wallet is now connected to your account",
|
||||||
});
|
});
|
||||||
navigate("/dashboard?tab=connections");
|
navigate("/dashboard?tab=connections");
|
||||||
return;
|
return;
|
||||||
|
|
@ -93,11 +94,21 @@ export default function Web3Callback() {
|
||||||
if (account && !authLoading) {
|
if (account && !authLoading) {
|
||||||
handleWeb3Auth();
|
handleWeb3Auth();
|
||||||
}
|
}
|
||||||
}, [account, authLoading, signMessage, user, navigate, toastError, toastSuccess]);
|
}, [
|
||||||
|
account,
|
||||||
|
authLoading,
|
||||||
|
signMessage,
|
||||||
|
user,
|
||||||
|
navigate,
|
||||||
|
toastError,
|
||||||
|
toastSuccess,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoadingScreen
|
<LoadingScreen
|
||||||
message={isProcessing ? "Verifying your wallet..." : "Connecting wallet..."}
|
message={
|
||||||
|
isProcessing ? "Verifying your wallet..." : "Connecting wallet..."
|
||||||
|
}
|
||||||
showProgress={true}
|
showProgress={true}
|
||||||
duration={5000}
|
duration={5000}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
### Step 1: Prepare the Bot Directory
|
### Step 1: Prepare the Bot Directory
|
||||||
|
|
||||||
Ensure all bot files are committed:
|
Ensure all bot files are committed:
|
||||||
|
|
||||||
```
|
```
|
||||||
code/discord-bot/
|
code/discord-bot/
|
||||||
├── bot.js
|
├── bot.js
|
||||||
|
|
@ -53,16 +54,19 @@ NODE_ENV=production
|
||||||
In Spaceship Application Settings:
|
In Spaceship Application Settings:
|
||||||
|
|
||||||
**Build Command:**
|
**Build Command:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
**Start Command:**
|
**Start Command:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
**Root Directory:**
|
**Root Directory:**
|
||||||
|
|
||||||
```
|
```
|
||||||
code/discord-bot
|
code/discord-bot
|
||||||
```
|
```
|
||||||
|
|
@ -80,6 +84,7 @@ code/discord-bot
|
||||||
### Step 6: Verify Bot is Online
|
### Step 6: Verify Bot is Online
|
||||||
|
|
||||||
Once deployed:
|
Once deployed:
|
||||||
|
|
||||||
1. Go to your Discord server
|
1. Go to your Discord server
|
||||||
2. Type `/verify` - the command autocomplete should appear
|
2. Type `/verify` - the command autocomplete should appear
|
||||||
3. Bot should be online with status "Listening to /verify to link your AeThex account"
|
3. Bot should be online with status "Listening to /verify to link your AeThex account"
|
||||||
|
|
@ -87,6 +92,7 @@ Once deployed:
|
||||||
## 📡 Discord Bot Endpoints
|
## 📡 Discord Bot Endpoints
|
||||||
|
|
||||||
The bot will be accessible at:
|
The bot will be accessible at:
|
||||||
|
|
||||||
```
|
```
|
||||||
https://<your-spaceship-domain>/
|
https://<your-spaceship-domain>/
|
||||||
```
|
```
|
||||||
|
|
@ -96,11 +102,13 @@ The bot uses Discord's WebSocket connection (not HTTP), so it doesn't need to ex
|
||||||
## 🔌 API Integration
|
## 🔌 API Integration
|
||||||
|
|
||||||
Frontend calls to link Discord accounts:
|
Frontend calls to link Discord accounts:
|
||||||
|
|
||||||
- **Endpoint:** `POST /api/discord/link`
|
- **Endpoint:** `POST /api/discord/link`
|
||||||
- **Body:** `{ verification_code, user_id }`
|
- **Body:** `{ verification_code, user_id }`
|
||||||
- **Response:** `{ success: true, message: "..." }`
|
- **Response:** `{ success: true, message: "..." }`
|
||||||
|
|
||||||
Discord Verify page (`/discord-verify?code=XXX`) will automatically:
|
Discord Verify page (`/discord-verify?code=XXX`) will automatically:
|
||||||
|
|
||||||
1. Call `/api/discord/link` with the verification code
|
1. Call `/api/discord/link` with the verification code
|
||||||
2. Link the Discord ID to the AeThex user account
|
2. Link the Discord ID to the AeThex user account
|
||||||
3. Redirect to dashboard on success
|
3. Redirect to dashboard on success
|
||||||
|
|
@ -108,22 +116,26 @@ Discord Verify page (`/discord-verify?code=XXX`) will automatically:
|
||||||
## 🛠️ Debugging
|
## 🛠️ Debugging
|
||||||
|
|
||||||
### Check bot logs on Spaceship:
|
### Check bot logs on Spaceship:
|
||||||
|
|
||||||
- Application → Logs
|
- Application → Logs
|
||||||
- Filter for "bot.js" or "error"
|
- Filter for "bot.js" or "error"
|
||||||
|
|
||||||
### Common issues:
|
### Common issues:
|
||||||
|
|
||||||
**"Discord bot not responding to commands"**
|
**"Discord bot not responding to commands"**
|
||||||
|
|
||||||
- Check: `DISCORD_BOT_TOKEN` is correct
|
- Check: `DISCORD_BOT_TOKEN` is correct
|
||||||
- Check: Bot is added to the Discord server with "applications.commands" scope
|
- Check: Bot is added to the Discord server with "applications.commands" scope
|
||||||
- Check: Spaceship logs show "✅ Logged in"
|
- Check: Spaceship logs show "✅ Logged in"
|
||||||
|
|
||||||
**"Supabase verification fails"**
|
**"Supabase verification fails"**
|
||||||
|
|
||||||
- Check: `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE` are correct
|
- Check: `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE` are correct
|
||||||
- Check: `discord_links` and `discord_verifications` tables exist
|
- Check: `discord_links` and `discord_verifications` tables exist
|
||||||
- Run migration: `code/supabase/migrations/20250107_add_discord_integration.sql`
|
- Run migration: `code/supabase/migrations/20250107_add_discord_integration.sql`
|
||||||
|
|
||||||
**"Slash commands not appearing in Discord"**
|
**"Slash commands not appearing in Discord"**
|
||||||
|
|
||||||
- Check: Logs show "✅ Successfully registered X slash commands"
|
- Check: Logs show "✅ Successfully registered X slash commands"
|
||||||
- Discord may need 1-2 minutes to sync commands
|
- Discord may need 1-2 minutes to sync commands
|
||||||
- Try typing `/` in Discord to force refresh
|
- Try typing `/` in Discord to force refresh
|
||||||
|
|
@ -132,12 +144,14 @@ Discord Verify page (`/discord-verify?code=XXX`) will automatically:
|
||||||
## 📊 Monitoring
|
## 📊 Monitoring
|
||||||
|
|
||||||
### Key metrics to monitor:
|
### Key metrics to monitor:
|
||||||
|
|
||||||
- Bot uptime (should be 24/7)
|
- Bot uptime (should be 24/7)
|
||||||
- Command usage (in Supabase)
|
- Command usage (in Supabase)
|
||||||
- Verification code usage (in Supabase)
|
- Verification code usage (in Supabase)
|
||||||
- Discord role sync success rate
|
- Discord role sync success rate
|
||||||
|
|
||||||
### View in Admin Dashboard:
|
### View in Admin Dashboard:
|
||||||
|
|
||||||
- AeThex Admin Panel → Discord Management tab
|
- AeThex Admin Panel → Discord Management tab
|
||||||
- Shows:
|
- Shows:
|
||||||
- Bot status
|
- Bot status
|
||||||
|
|
@ -156,6 +170,7 @@ Discord Verify page (`/discord-verify?code=XXX`) will automatically:
|
||||||
## 🆘 Support
|
## 🆘 Support
|
||||||
|
|
||||||
For issues:
|
For issues:
|
||||||
|
|
||||||
1. Check Spaceship logs
|
1. Check Spaceship logs
|
||||||
2. Review `/api/discord/link` endpoint response
|
2. Review `/api/discord/link` endpoint response
|
||||||
3. Verify all environment variables are set correctly
|
3. Verify all environment variables are set correctly
|
||||||
|
|
@ -177,6 +192,7 @@ Run this migration on your AeThex Supabase:
|
||||||
## 🎉 You're All Set!
|
## 🎉 You're All Set!
|
||||||
|
|
||||||
Once deployed, users can:
|
Once deployed, users can:
|
||||||
|
|
||||||
1. Click "Link Discord" in their profile settings
|
1. Click "Link Discord" in their profile settings
|
||||||
2. Type `/verify` in Discord
|
2. Type `/verify` in Discord
|
||||||
3. Click the verification link
|
3. Click the verification link
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,20 @@
|
||||||
const { Client, GatewayIntentBits, REST, Routes, Collection, EmbedBuilder } = require('discord.js');
|
const {
|
||||||
const { createClient } = require('@supabase/supabase-js');
|
Client,
|
||||||
const fs = require('fs');
|
GatewayIntentBits,
|
||||||
const path = require('path');
|
REST,
|
||||||
require('dotenv').config();
|
Routes,
|
||||||
|
Collection,
|
||||||
|
EmbedBuilder,
|
||||||
|
} = require("discord.js");
|
||||||
|
const { createClient } = require("@supabase/supabase-js");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
require("dotenv").config();
|
||||||
|
|
||||||
// Initialize Discord client
|
// Initialize Discord client
|
||||||
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.DirectMessages] });
|
const client = new Client({
|
||||||
|
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.DirectMessages],
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize Supabase
|
// Initialize Supabase
|
||||||
const supabase = createClient(
|
const supabase = createClient(
|
||||||
|
|
@ -17,35 +26,41 @@ const supabase = createClient(
|
||||||
client.commands = new Collection();
|
client.commands = new Collection();
|
||||||
|
|
||||||
// Load commands from commands directory
|
// Load commands from commands directory
|
||||||
const commandsPath = path.join(__dirname, 'commands');
|
const commandsPath = path.join(__dirname, "commands");
|
||||||
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
const commandFiles = fs
|
||||||
|
.readdirSync(commandsPath)
|
||||||
|
.filter((file) => file.endsWith(".js"));
|
||||||
|
|
||||||
for (const file of commandFiles) {
|
for (const file of commandFiles) {
|
||||||
const filePath = path.join(commandsPath, file);
|
const filePath = path.join(commandsPath, file);
|
||||||
const command = require(filePath);
|
const command = require(filePath);
|
||||||
if ('data' in command && 'execute' in command) {
|
if ("data" in command && "execute" in command) {
|
||||||
client.commands.set(command.data.name, command);
|
client.commands.set(command.data.name, command);
|
||||||
console.log(`✅ Loaded command: ${command.data.name}`);
|
console.log(`✅ Loaded command: ${command.data.name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bot ready event
|
// Bot ready event
|
||||||
client.once('ready', () => {
|
client.once("ready", () => {
|
||||||
console.log(`✅ Bot logged in as ${client.user.tag}`);
|
console.log(`✅ Bot logged in as ${client.user.tag}`);
|
||||||
console.log(`📡 Listening in ${client.guilds.cache.size} server(s)`);
|
console.log(`📡 Listening in ${client.guilds.cache.size} server(s)`);
|
||||||
|
|
||||||
// Set bot status
|
// Set bot status
|
||||||
client.user.setActivity('/verify to link your AeThex account', { type: 'LISTENING' });
|
client.user.setActivity("/verify to link your AeThex account", {
|
||||||
|
type: "LISTENING",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Slash command interaction handler
|
// Slash command interaction handler
|
||||||
client.on('interactionCreate', async interaction => {
|
client.on("interactionCreate", async (interaction) => {
|
||||||
if (!interaction.isChatInputCommand()) return;
|
if (!interaction.isChatInputCommand()) return;
|
||||||
|
|
||||||
const command = client.commands.get(interaction.commandName);
|
const command = client.commands.get(interaction.commandName);
|
||||||
|
|
||||||
if (!command) {
|
if (!command) {
|
||||||
console.warn(`⚠️ No command matching ${interaction.commandName} was found.`);
|
console.warn(
|
||||||
|
`⚠️ No command matching ${interaction.commandName} was found.`,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,12 +68,12 @@ client.on('interactionCreate', async interaction => {
|
||||||
await command.execute(interaction, supabase, client);
|
await command.execute(interaction, supabase, client);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Error executing ${interaction.commandName}:`, error);
|
console.error(`❌ Error executing ${interaction.commandName}:`, error);
|
||||||
|
|
||||||
const errorEmbed = new EmbedBuilder()
|
const errorEmbed = new EmbedBuilder()
|
||||||
.setColor(0xFF0000)
|
.setColor(0xff0000)
|
||||||
.setTitle('❌ Command Error')
|
.setTitle("❌ Command Error")
|
||||||
.setDescription('There was an error while executing this command.')
|
.setDescription("There was an error while executing this command.")
|
||||||
.setFooter({ text: 'Contact support if this persists' });
|
.setFooter({ text: "Contact support if this persists" });
|
||||||
|
|
||||||
if (interaction.replied || interaction.deferred) {
|
if (interaction.replied || interaction.deferred) {
|
||||||
await interaction.followUp({ embeds: [errorEmbed], ephemeral: true });
|
await interaction.followUp({ embeds: [errorEmbed], ephemeral: true });
|
||||||
|
|
@ -76,7 +91,9 @@ async function registerCommands() {
|
||||||
commands.push(command.data.toJSON());
|
commands.push(command.data.toJSON());
|
||||||
}
|
}
|
||||||
|
|
||||||
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_BOT_TOKEN);
|
const rest = new REST({ version: "10" }).setToken(
|
||||||
|
process.env.DISCORD_BOT_TOKEN,
|
||||||
|
);
|
||||||
|
|
||||||
console.log(`📝 Registering ${commands.length} slash commands...`);
|
console.log(`📝 Registering ${commands.length} slash commands...`);
|
||||||
|
|
||||||
|
|
@ -87,24 +104,24 @@ async function registerCommands() {
|
||||||
|
|
||||||
console.log(`✅ Successfully registered ${data.length} slash commands.`);
|
console.log(`✅ Successfully registered ${data.length} slash commands.`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error registering commands:', error);
|
console.error("❌ Error registering commands:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login and register commands
|
// Login and register commands
|
||||||
client.login(process.env.DISCORD_BOT_TOKEN);
|
client.login(process.env.DISCORD_BOT_TOKEN);
|
||||||
|
|
||||||
client.once('ready', async () => {
|
client.once("ready", async () => {
|
||||||
await registerCommands();
|
await registerCommands();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Error handling
|
// Error handling
|
||||||
process.on('unhandledRejection', error => {
|
process.on("unhandledRejection", (error) => {
|
||||||
console.error('❌ Unhandled Promise Rejection:', error);
|
console.error("❌ Unhandled Promise Rejection:", error);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('uncaughtException', error => {
|
process.on("uncaughtException", (error) => {
|
||||||
console.error('❌ Uncaught Exception:', error);
|
console.error("❌ Uncaught Exception:", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,75 +1,92 @@
|
||||||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName('profile')
|
.setName("profile")
|
||||||
.setDescription('View your AeThex profile in Discord'),
|
.setDescription("View your AeThex profile in Discord"),
|
||||||
|
|
||||||
async execute(interaction, supabase) {
|
async execute(interaction, supabase) {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: link } = await supabase
|
const { data: link } = await supabase
|
||||||
.from('discord_links')
|
.from("discord_links")
|
||||||
.select('user_id, primary_arm')
|
.select("user_id, primary_arm")
|
||||||
.eq('discord_id', interaction.user.id)
|
.eq("discord_id", interaction.user.id)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (!link) {
|
if (!link) {
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setColor(0xFF6B6B)
|
.setColor(0xff6b6b)
|
||||||
.setTitle('❌ Not Linked')
|
.setTitle("❌ Not Linked")
|
||||||
.setDescription('You must link your Discord account to AeThex first.\nUse `/verify` to get started.');
|
.setDescription(
|
||||||
|
"You must link your Discord account to AeThex first.\nUse `/verify` to get started.",
|
||||||
|
);
|
||||||
|
|
||||||
return await interaction.editReply({ embeds: [embed] });
|
return await interaction.editReply({ embeds: [embed] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: profile } = await supabase
|
const { data: profile } = await supabase
|
||||||
.from('user_profiles')
|
.from("user_profiles")
|
||||||
.select('*')
|
.select("*")
|
||||||
.eq('id', link.user_id)
|
.eq("id", link.user_id)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setColor(0xFF6B6B)
|
.setColor(0xff6b6b)
|
||||||
.setTitle('❌ Profile Not Found')
|
.setTitle("❌ Profile Not Found")
|
||||||
.setDescription('Your AeThex profile could not be found.');
|
.setDescription("Your AeThex profile could not be found.");
|
||||||
|
|
||||||
return await interaction.editReply({ embeds: [embed] });
|
return await interaction.editReply({ embeds: [embed] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const armEmojis = {
|
const armEmojis = {
|
||||||
labs: '🧪',
|
labs: "🧪",
|
||||||
gameforge: '🎮',
|
gameforge: "🎮",
|
||||||
corp: '💼',
|
corp: "💼",
|
||||||
foundation: '🤝',
|
foundation: "🤝",
|
||||||
devlink: '💻',
|
devlink: "💻",
|
||||||
};
|
};
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setColor(0x7289DA)
|
.setColor(0x7289da)
|
||||||
.setTitle(`${profile.full_name || 'AeThex User'}'s Profile`)
|
.setTitle(`${profile.full_name || "AeThex User"}'s Profile`)
|
||||||
.setThumbnail(profile.avatar_url || 'https://aethex.dev/placeholder.svg')
|
.setThumbnail(
|
||||||
.addFields(
|
profile.avatar_url || "https://aethex.dev/placeholder.svg",
|
||||||
{ name: '👤 Username', value: profile.username || 'N/A', inline: true },
|
|
||||||
{ name: `${armEmojis[link.primary_arm] || '⚔️'} Primary Realm`, value: link.primary_arm || 'Not set', inline: true },
|
|
||||||
{ name: '📊 Role', value: profile.user_type || 'community_member', inline: true },
|
|
||||||
{ name: '📝 Bio', value: profile.bio || 'No bio set', inline: false },
|
|
||||||
)
|
)
|
||||||
.addFields(
|
.addFields(
|
||||||
{ name: '🔗 Links', value: `[Visit Full Profile](https://aethex.dev/creators/${profile.username})` },
|
{
|
||||||
|
name: "👤 Username",
|
||||||
|
value: profile.username || "N/A",
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `${armEmojis[link.primary_arm] || "⚔️"} Primary Realm`,
|
||||||
|
value: link.primary_arm || "Not set",
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "📊 Role",
|
||||||
|
value: profile.user_type || "community_member",
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{ name: "📝 Bio", value: profile.bio || "No bio set", inline: false },
|
||||||
)
|
)
|
||||||
.setFooter({ text: 'AeThex | Your Web3 Creator Hub' });
|
.addFields({
|
||||||
|
name: "🔗 Links",
|
||||||
|
value: `[Visit Full Profile](https://aethex.dev/creators/${profile.username})`,
|
||||||
|
})
|
||||||
|
.setFooter({ text: "AeThex | Your Web3 Creator Hub" });
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [embed] });
|
await interaction.editReply({ embeds: [embed] });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Profile command error:', error);
|
console.error("Profile command error:", error);
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setColor(0xFF0000)
|
.setColor(0xff0000)
|
||||||
.setTitle('❌ Error')
|
.setTitle("❌ Error")
|
||||||
.setDescription('Failed to fetch profile. Please try again.');
|
.setDescription("Failed to fetch profile. Please try again.");
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [embed] });
|
await interaction.editReply({ embeds: [embed] });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,61 @@
|
||||||
const { SlashCommandBuilder, EmbedBuilder, StringSelectMenuBuilder, ActionRowBuilder } = require('discord.js');
|
const {
|
||||||
|
SlashCommandBuilder,
|
||||||
|
EmbedBuilder,
|
||||||
|
StringSelectMenuBuilder,
|
||||||
|
ActionRowBuilder,
|
||||||
|
} = require("discord.js");
|
||||||
|
|
||||||
const REALMS = [
|
const REALMS = [
|
||||||
{ value: 'labs', label: '🧪 Labs', description: 'Research & Development' },
|
{ value: "labs", label: "🧪 Labs", description: "Research & Development" },
|
||||||
{ value: 'gameforge', label: '🎮 GameForge', description: 'Game Development' },
|
{
|
||||||
{ value: 'corp', label: '💼 Corp', description: 'Enterprise Solutions' },
|
value: "gameforge",
|
||||||
{ value: 'foundation', label: '🤝 Foundation', description: 'Community & Education' },
|
label: "🎮 GameForge",
|
||||||
{ value: 'devlink', label: '💻 Dev-Link', description: 'Professional Networking' },
|
description: "Game Development",
|
||||||
|
},
|
||||||
|
{ value: "corp", label: "💼 Corp", description: "Enterprise Solutions" },
|
||||||
|
{
|
||||||
|
value: "foundation",
|
||||||
|
label: "🤝 Foundation",
|
||||||
|
description: "Community & Education",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "devlink",
|
||||||
|
label: "💻 Dev-Link",
|
||||||
|
description: "Professional Networking",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName('set-realm')
|
.setName("set-realm")
|
||||||
.setDescription('Set your primary AeThex realm/arm'),
|
.setDescription("Set your primary AeThex realm/arm"),
|
||||||
|
|
||||||
async execute(interaction, supabase) {
|
async execute(interaction, supabase) {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: link } = await supabase
|
const { data: link } = await supabase
|
||||||
.from('discord_links')
|
.from("discord_links")
|
||||||
.select('user_id, primary_arm')
|
.select("user_id, primary_arm")
|
||||||
.eq('discord_id', interaction.user.id)
|
.eq("discord_id", interaction.user.id)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (!link) {
|
if (!link) {
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setColor(0xFF6B6B)
|
.setColor(0xff6b6b)
|
||||||
.setTitle('❌ Not Linked')
|
.setTitle("❌ Not Linked")
|
||||||
.setDescription('You must link your Discord account to AeThex first.\nUse `/verify` to get started.');
|
.setDescription(
|
||||||
|
"You must link your Discord account to AeThex first.\nUse `/verify` to get started.",
|
||||||
|
);
|
||||||
|
|
||||||
return await interaction.editReply({ embeds: [embed] });
|
return await interaction.editReply({ embeds: [embed] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const select = new StringSelectMenuBuilder()
|
const select = new StringSelectMenuBuilder()
|
||||||
.setCustomId('select_realm')
|
.setCustomId("select_realm")
|
||||||
.setPlaceholder('Choose your primary realm')
|
.setPlaceholder("Choose your primary realm")
|
||||||
.addOptions(
|
.addOptions(
|
||||||
REALMS.map(realm => ({
|
REALMS.map((realm) => ({
|
||||||
label: realm.label,
|
label: realm.label,
|
||||||
description: realm.description,
|
description: realm.description,
|
||||||
value: realm.value,
|
value: realm.value,
|
||||||
|
|
@ -47,48 +66,60 @@ module.exports = {
|
||||||
const row = new ActionRowBuilder().addComponents(select);
|
const row = new ActionRowBuilder().addComponents(select);
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setColor(0x7289DA)
|
.setColor(0x7289da)
|
||||||
.setTitle('⚔️ Choose Your Realm')
|
.setTitle("⚔️ Choose Your Realm")
|
||||||
.setDescription('Select your primary AeThex realm. This determines your main Discord role.')
|
.setDescription(
|
||||||
.addFields(
|
"Select your primary AeThex realm. This determines your main Discord role.",
|
||||||
{ name: 'Current Realm', value: link.primary_arm || 'Not set' },
|
)
|
||||||
);
|
.addFields({
|
||||||
|
name: "Current Realm",
|
||||||
|
value: link.primary_arm || "Not set",
|
||||||
|
});
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [embed], components: [row] });
|
await interaction.editReply({ embeds: [embed], components: [row] });
|
||||||
|
|
||||||
const filter = i => i.user.id === interaction.user.id && i.customId === 'select_realm';
|
const filter = (i) =>
|
||||||
const collector = interaction.channel.createMessageComponentCollector({ filter, time: 60000 });
|
i.user.id === interaction.user.id && i.customId === "select_realm";
|
||||||
|
const collector = interaction.channel.createMessageComponentCollector({
|
||||||
|
filter,
|
||||||
|
time: 60000,
|
||||||
|
});
|
||||||
|
|
||||||
collector.on('collect', async i => {
|
collector.on("collect", async (i) => {
|
||||||
const selectedRealm = i.values[0];
|
const selectedRealm = i.values[0];
|
||||||
|
|
||||||
await supabase
|
await supabase
|
||||||
.from('discord_links')
|
.from("discord_links")
|
||||||
.update({ primary_arm: selectedRealm })
|
.update({ primary_arm: selectedRealm })
|
||||||
.eq('discord_id', interaction.user.id);
|
.eq("discord_id", interaction.user.id);
|
||||||
|
|
||||||
const realm = REALMS.find(r => r.value === selectedRealm);
|
const realm = REALMS.find((r) => r.value === selectedRealm);
|
||||||
|
|
||||||
const confirmEmbed = new EmbedBuilder()
|
const confirmEmbed = new EmbedBuilder()
|
||||||
.setColor(0x00FF00)
|
.setColor(0x00ff00)
|
||||||
.setTitle('✅ Realm Set')
|
.setTitle("✅ Realm Set")
|
||||||
.setDescription(`Your primary realm is now **${realm.label}**\n\nYou'll be assigned the corresponding Discord role.`);
|
.setDescription(
|
||||||
|
`Your primary realm is now **${realm.label}**\n\nYou'll be assigned the corresponding Discord role.`,
|
||||||
|
);
|
||||||
|
|
||||||
await i.update({ embeds: [confirmEmbed], components: [] });
|
await i.update({ embeds: [confirmEmbed], components: [] });
|
||||||
});
|
});
|
||||||
|
|
||||||
collector.on('end', collected => {
|
collector.on("end", (collected) => {
|
||||||
if (collected.size === 0) {
|
if (collected.size === 0) {
|
||||||
interaction.editReply({ content: 'Realm selection timed out.', components: [] });
|
interaction.editReply({
|
||||||
|
content: "Realm selection timed out.",
|
||||||
|
components: [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Set-realm command error:', error);
|
console.error("Set-realm command error:", error);
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setColor(0xFF0000)
|
.setColor(0xff0000)
|
||||||
.setTitle('❌ Error')
|
.setTitle("❌ Error")
|
||||||
.setDescription('Failed to update realm. Please try again.');
|
.setDescription("Failed to update realm. Please try again.");
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [embed] });
|
await interaction.editReply({ embeds: [embed] });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,49 @@
|
||||||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName('unlink')
|
.setName("unlink")
|
||||||
.setDescription('Unlink your Discord account from AeThex'),
|
.setDescription("Unlink your Discord account from AeThex"),
|
||||||
|
|
||||||
async execute(interaction, supabase) {
|
async execute(interaction, supabase) {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: link } = await supabase
|
const { data: link } = await supabase
|
||||||
.from('discord_links')
|
.from("discord_links")
|
||||||
.select('*')
|
.select("*")
|
||||||
.eq('discord_id', interaction.user.id)
|
.eq("discord_id", interaction.user.id)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (!link) {
|
if (!link) {
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setColor(0xFF6B6B)
|
.setColor(0xff6b6b)
|
||||||
.setTitle('ℹ️ Not Linked')
|
.setTitle("ℹ️ Not Linked")
|
||||||
.setDescription('Your Discord account is not linked to AeThex.');
|
.setDescription("Your Discord account is not linked to AeThex.");
|
||||||
|
|
||||||
return await interaction.editReply({ embeds: [embed] });
|
return await interaction.editReply({ embeds: [embed] });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the link
|
// Delete the link
|
||||||
await supabase
|
await supabase
|
||||||
.from('discord_links')
|
.from("discord_links")
|
||||||
.delete()
|
.delete()
|
||||||
.eq('discord_id', interaction.user.id);
|
.eq("discord_id", interaction.user.id);
|
||||||
|
|
||||||
// Remove Discord roles from user
|
// Remove Discord roles from user
|
||||||
const guild = interaction.guild;
|
const guild = interaction.guild;
|
||||||
const member = await guild.members.fetch(interaction.user.id);
|
const member = await guild.members.fetch(interaction.user.id);
|
||||||
|
|
||||||
// Find and remove all AeThex-related roles
|
// Find and remove all AeThex-related roles
|
||||||
const rolesToRemove = member.roles.cache.filter(role =>
|
const rolesToRemove = member.roles.cache.filter(
|
||||||
role.name.includes('Labs') ||
|
(role) =>
|
||||||
role.name.includes('GameForge') ||
|
role.name.includes("Labs") ||
|
||||||
role.name.includes('Corp') ||
|
role.name.includes("GameForge") ||
|
||||||
role.name.includes('Foundation') ||
|
role.name.includes("Corp") ||
|
||||||
role.name.includes('Dev-Link') ||
|
role.name.includes("Foundation") ||
|
||||||
role.name.includes('Premium') ||
|
role.name.includes("Dev-Link") ||
|
||||||
role.name.includes('Creator')
|
role.name.includes("Premium") ||
|
||||||
|
role.name.includes("Creator"),
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const [, role] of rolesToRemove) {
|
for (const [, role] of rolesToRemove) {
|
||||||
|
|
@ -54,18 +55,20 @@ module.exports = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setColor(0x00FF00)
|
.setColor(0x00ff00)
|
||||||
.setTitle('✅ Account Unlinked')
|
.setTitle("✅ Account Unlinked")
|
||||||
.setDescription('Your Discord account has been unlinked from AeThex.\nAll associated roles have been removed.');
|
.setDescription(
|
||||||
|
"Your Discord account has been unlinked from AeThex.\nAll associated roles have been removed.",
|
||||||
|
);
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [embed] });
|
await interaction.editReply({ embeds: [embed] });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Unlink command error:', error);
|
console.error("Unlink command error:", error);
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setColor(0xFF0000)
|
.setColor(0xff0000)
|
||||||
.setTitle('❌ Error')
|
.setTitle("❌ Error")
|
||||||
.setDescription('Failed to unlink account. Please try again.');
|
.setDescription("Failed to unlink account. Please try again.");
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [embed] });
|
await interaction.editReply({ embeds: [embed] });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,96 @@
|
||||||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName('verify-role')
|
.setName("verify-role")
|
||||||
.setDescription('Check your AeThex-assigned Discord roles'),
|
.setDescription("Check your AeThex-assigned Discord roles"),
|
||||||
|
|
||||||
async execute(interaction, supabase) {
|
async execute(interaction, supabase) {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: link } = await supabase
|
const { data: link } = await supabase
|
||||||
.from('discord_links')
|
.from("discord_links")
|
||||||
.select('user_id, primary_arm')
|
.select("user_id, primary_arm")
|
||||||
.eq('discord_id', interaction.user.id)
|
.eq("discord_id", interaction.user.id)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (!link) {
|
if (!link) {
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setColor(0xFF6B6B)
|
.setColor(0xff6b6b)
|
||||||
.setTitle('❌ Not Linked')
|
.setTitle("❌ Not Linked")
|
||||||
.setDescription('You must link your Discord account to AeThex first.\nUse `/verify` to get started.');
|
.setDescription(
|
||||||
|
"You must link your Discord account to AeThex first.\nUse `/verify` to get started.",
|
||||||
|
);
|
||||||
|
|
||||||
return await interaction.editReply({ embeds: [embed] });
|
return await interaction.editReply({ embeds: [embed] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: profile } = await supabase
|
const { data: profile } = await supabase
|
||||||
.from('user_profiles')
|
.from("user_profiles")
|
||||||
.select('user_type')
|
.select("user_type")
|
||||||
.eq('id', link.user_id)
|
.eq("id", link.user_id)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
const { data: mappings } = await supabase
|
const { data: mappings } = await supabase
|
||||||
.from('discord_role_mappings')
|
.from("discord_role_mappings")
|
||||||
.select('discord_role')
|
.select("discord_role")
|
||||||
.eq('arm', link.primary_arm)
|
.eq("arm", link.primary_arm)
|
||||||
.eq('user_type', profile?.user_type || 'community_member');
|
.eq("user_type", profile?.user_type || "community_member");
|
||||||
|
|
||||||
const member = await interaction.guild.members.fetch(interaction.user.id);
|
const member = await interaction.guild.members.fetch(interaction.user.id);
|
||||||
const aethexRoles = member.roles.cache.filter(role =>
|
const aethexRoles = member.roles.cache.filter(
|
||||||
role.name.includes('Labs') ||
|
(role) =>
|
||||||
role.name.includes('GameForge') ||
|
role.name.includes("Labs") ||
|
||||||
role.name.includes('Corp') ||
|
role.name.includes("GameForge") ||
|
||||||
role.name.includes('Foundation') ||
|
role.name.includes("Corp") ||
|
||||||
role.name.includes('Dev-Link') ||
|
role.name.includes("Foundation") ||
|
||||||
role.name.includes('Premium') ||
|
role.name.includes("Dev-Link") ||
|
||||||
role.name.includes('Creator')
|
role.name.includes("Premium") ||
|
||||||
|
role.name.includes("Creator"),
|
||||||
);
|
);
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setColor(0x7289DA)
|
.setColor(0x7289da)
|
||||||
.setTitle('🔐 Your AeThex Roles')
|
.setTitle("🔐 Your AeThex Roles")
|
||||||
.addFields(
|
.addFields(
|
||||||
{ name: '⚔️ Primary Realm', value: link.primary_arm || 'Not set', inline: true },
|
{
|
||||||
{ name: '👤 User Type', value: profile?.user_type || 'community_member', inline: true },
|
name: "⚔️ Primary Realm",
|
||||||
{ name: '🎭 Discord Roles', value: aethexRoles.size > 0 ? aethexRoles.map(r => r.name).join(', ') : 'None assigned yet' },
|
value: link.primary_arm || "Not set",
|
||||||
{ name: '📋 Expected Roles', value: mappings?.length > 0 ? mappings.map(m => m.discord_role).join(', ') : 'No mappings found' },
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "👤 User Type",
|
||||||
|
value: profile?.user_type || "community_member",
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "🎭 Discord Roles",
|
||||||
|
value:
|
||||||
|
aethexRoles.size > 0
|
||||||
|
? aethexRoles.map((r) => r.name).join(", ")
|
||||||
|
: "None assigned yet",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "📋 Expected Roles",
|
||||||
|
value:
|
||||||
|
mappings?.length > 0
|
||||||
|
? mappings.map((m) => m.discord_role).join(", ")
|
||||||
|
: "No mappings found",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.setFooter({ text: 'Roles are assigned automatically based on your AeThex profile' });
|
.setFooter({
|
||||||
|
text: "Roles are assigned automatically based on your AeThex profile",
|
||||||
|
});
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [embed] });
|
await interaction.editReply({ embeds: [embed] });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Verify-role command error:', error);
|
console.error("Verify-role command error:", error);
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setColor(0xFF0000)
|
.setColor(0xff0000)
|
||||||
.setTitle('❌ Error')
|
.setTitle("❌ Error")
|
||||||
.setDescription('Failed to verify roles. Please try again.');
|
.setDescription("Failed to verify roles. Please try again.");
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [embed] });
|
await interaction.editReply({ embeds: [embed] });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,46 @@
|
||||||
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
|
const {
|
||||||
|
SlashCommandBuilder,
|
||||||
|
EmbedBuilder,
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonBuilder,
|
||||||
|
ButtonStyle,
|
||||||
|
} = require("discord.js");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName('verify')
|
.setName("verify")
|
||||||
.setDescription('Link your Discord account to your AeThex account'),
|
.setDescription("Link your Discord account to your AeThex account"),
|
||||||
|
|
||||||
async execute(interaction, supabase) {
|
async execute(interaction, supabase) {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: existingLink } = await supabase
|
const { data: existingLink } = await supabase
|
||||||
.from('discord_links')
|
.from("discord_links")
|
||||||
.select('*')
|
.select("*")
|
||||||
.eq('discord_id', interaction.user.id)
|
.eq("discord_id", interaction.user.id)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (existingLink) {
|
if (existingLink) {
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setColor(0x00FF00)
|
.setColor(0x00ff00)
|
||||||
.setTitle('✅ Already Linked')
|
.setTitle("✅ Already Linked")
|
||||||
.setDescription(`Your Discord account is already linked to AeThex (User ID: ${existingLink.user_id})`);
|
.setDescription(
|
||||||
|
`Your Discord account is already linked to AeThex (User ID: ${existingLink.user_id})`,
|
||||||
|
);
|
||||||
|
|
||||||
return await interaction.editReply({ embeds: [embed] });
|
return await interaction.editReply({ embeds: [embed] });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate verification code
|
// Generate verification code
|
||||||
const verificationCode = Math.random().toString(36).substring(2, 8).toUpperCase();
|
const verificationCode = Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.substring(2, 8)
|
||||||
|
.toUpperCase();
|
||||||
const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes
|
const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes
|
||||||
|
|
||||||
// Store verification code
|
// Store verification code
|
||||||
await supabase.from('discord_verifications').insert({
|
await supabase.from("discord_verifications").insert({
|
||||||
discord_id: interaction.user.id,
|
discord_id: interaction.user.id,
|
||||||
verification_code: verificationCode,
|
verification_code: verificationCode,
|
||||||
expires_at: expiresAt.toISOString(),
|
expires_at: expiresAt.toISOString(),
|
||||||
|
|
@ -38,30 +49,34 @@ module.exports = {
|
||||||
const verifyUrl = `https://aethex.dev/discord-verify?code=${verificationCode}`;
|
const verifyUrl = `https://aethex.dev/discord-verify?code=${verificationCode}`;
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setColor(0x7289DA)
|
.setColor(0x7289da)
|
||||||
.setTitle('🔗 Link Your AeThex Account')
|
.setTitle("🔗 Link Your AeThex Account")
|
||||||
.setDescription('Click the button below to link your Discord account to AeThex.')
|
.setDescription(
|
||||||
.addFields(
|
"Click the button below to link your Discord account to AeThex.",
|
||||||
{ name: '⏱️ Expires In', value: '15 minutes' },
|
|
||||||
{ name: '📝 Verification Code', value: `\`${verificationCode}\`` },
|
|
||||||
)
|
)
|
||||||
.setFooter({ text: 'Your security code will expire in 15 minutes' });
|
.addFields(
|
||||||
|
{ name: "⏱️ Expires In", value: "15 minutes" },
|
||||||
|
{ name: "📝 Verification Code", value: `\`${verificationCode}\`` },
|
||||||
|
)
|
||||||
|
.setFooter({ text: "Your security code will expire in 15 minutes" });
|
||||||
|
|
||||||
const row = new ActionRowBuilder().addComponents(
|
const row = new ActionRowBuilder().addComponents(
|
||||||
new ButtonBuilder()
|
new ButtonBuilder()
|
||||||
.setLabel('Link Account')
|
.setLabel("Link Account")
|
||||||
.setStyle(ButtonStyle.Link)
|
.setStyle(ButtonStyle.Link)
|
||||||
.setURL(verifyUrl),
|
.setURL(verifyUrl),
|
||||||
);
|
);
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [embed], components: [row] });
|
await interaction.editReply({ embeds: [embed], components: [row] });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Verify command error:', error);
|
console.error("Verify command error:", error);
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setColor(0xFF0000)
|
.setColor(0xff0000)
|
||||||
.setTitle('❌ Error')
|
.setTitle("❌ Error")
|
||||||
.setDescription('Failed to generate verification code. Please try again.');
|
.setDescription(
|
||||||
|
"Failed to generate verification code. Please try again.",
|
||||||
|
);
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [embed] });
|
await interaction.editReply({ embeds: [embed] });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ This guide covers how to integrate AeThex authentication with game engines: Robl
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
AeThex provides a unified authentication system for games to:
|
AeThex provides a unified authentication system for games to:
|
||||||
|
|
||||||
- Authenticate players across different platforms
|
- Authenticate players across different platforms
|
||||||
- Link player accounts to Roblox, Ethereum wallets, and other providers
|
- Link player accounts to Roblox, Ethereum wallets, and other providers
|
||||||
- Store player profiles and achievements
|
- Store player profiles and achievements
|
||||||
|
|
@ -53,13 +54,13 @@ function AethexAuth:authenticate(playerId, playerName)
|
||||||
player_name = playerName,
|
player_name = playerName,
|
||||||
platform = "PC"
|
platform = "PC"
|
||||||
})
|
})
|
||||||
|
|
||||||
local response = game:GetService("HttpService"):PostAsync(
|
local response = game:GetService("HttpService"):PostAsync(
|
||||||
API_BASE .. "/games/game-auth",
|
API_BASE .. "/games/game-auth",
|
||||||
body,
|
body,
|
||||||
Enum.HttpContentType.ApplicationJson
|
Enum.HttpContentType.ApplicationJson
|
||||||
)
|
)
|
||||||
|
|
||||||
local data = game:GetService("HttpService"):JSONDecode(response)
|
local data = game:GetService("HttpService"):JSONDecode(response)
|
||||||
return data
|
return data
|
||||||
end
|
end
|
||||||
|
|
@ -73,7 +74,7 @@ function AethexAuth:verifyToken(sessionToken)
|
||||||
}),
|
}),
|
||||||
Enum.HttpContentType.ApplicationJson
|
Enum.HttpContentType.ApplicationJson
|
||||||
)
|
)
|
||||||
|
|
||||||
return game:GetService("HttpService"):JSONDecode(response)
|
return game:GetService("HttpService"):JSONDecode(response)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -88,7 +89,7 @@ local AethexAuth = require(game.ServerScriptService:WaitForChild("AethexAuth"))
|
||||||
|
|
||||||
Players.PlayerAdded:Connect(function(player)
|
Players.PlayerAdded:Connect(function(player)
|
||||||
local authResult = AethexAuth:authenticate(player.UserId, player.Name)
|
local authResult = AethexAuth:authenticate(player.UserId, player.Name)
|
||||||
|
|
||||||
if authResult.success then
|
if authResult.success then
|
||||||
player:SetAttribute("AethexSessionToken", authResult.session_token)
|
player:SetAttribute("AethexSessionToken", authResult.session_token)
|
||||||
player:SetAttribute("AethexUserId", authResult.user_id)
|
player:SetAttribute("AethexUserId", authResult.user_id)
|
||||||
|
|
@ -118,7 +119,7 @@ using System.Collections;
|
||||||
public class AethexAuth : MonoBehaviour
|
public class AethexAuth : MonoBehaviour
|
||||||
{
|
{
|
||||||
private const string API_BASE = "https://aethex.dev/api";
|
private const string API_BASE = "https://aethex.dev/api";
|
||||||
|
|
||||||
[System.Serializable]
|
[System.Serializable]
|
||||||
public class AuthResponse
|
public class AuthResponse
|
||||||
{
|
{
|
||||||
|
|
@ -129,7 +130,7 @@ public class AethexAuth : MonoBehaviour
|
||||||
public int expires_in;
|
public int expires_in;
|
||||||
public string error;
|
public string error;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerator AuthenticatePlayer(
|
public static IEnumerator AuthenticatePlayer(
|
||||||
string playerId,
|
string playerId,
|
||||||
string playerName,
|
string playerName,
|
||||||
|
|
@ -139,7 +140,7 @@ public class AethexAuth : MonoBehaviour
|
||||||
$"{API_BASE}/games/game-auth",
|
$"{API_BASE}/games/game-auth",
|
||||||
"POST"
|
"POST"
|
||||||
);
|
);
|
||||||
|
|
||||||
var requestBody = new AuthRequest
|
var requestBody = new AuthRequest
|
||||||
{
|
{
|
||||||
game = "unity",
|
game = "unity",
|
||||||
|
|
@ -147,14 +148,14 @@ public class AethexAuth : MonoBehaviour
|
||||||
player_name = playerName,
|
player_name = playerName,
|
||||||
platform = "PC"
|
platform = "PC"
|
||||||
};
|
};
|
||||||
|
|
||||||
string jsonBody = JsonUtility.ToJson(requestBody);
|
string jsonBody = JsonUtility.ToJson(requestBody);
|
||||||
request.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(jsonBody));
|
request.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(jsonBody));
|
||||||
request.downloadHandler = new DownloadHandlerBuffer();
|
request.downloadHandler = new DownloadHandlerBuffer();
|
||||||
request.SetRequestHeader("Content-Type", "application/json");
|
request.SetRequestHeader("Content-Type", "application/json");
|
||||||
|
|
||||||
yield return request.SendWebRequest();
|
yield return request.SendWebRequest();
|
||||||
|
|
||||||
if (request.result == UnityWebRequest.Result.Success)
|
if (request.result == UnityWebRequest.Result.Success)
|
||||||
{
|
{
|
||||||
var response = JsonUtility.FromJson<AuthResponse>(request.downloadHandler.text);
|
var response = JsonUtility.FromJson<AuthResponse>(request.downloadHandler.text);
|
||||||
|
|
@ -165,7 +166,7 @@ public class AethexAuth : MonoBehaviour
|
||||||
callback(new AuthResponse { error = request.error });
|
callback(new AuthResponse { error = request.error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[System.Serializable]
|
[System.Serializable]
|
||||||
private class AuthRequest
|
private class AuthRequest
|
||||||
{
|
{
|
||||||
|
|
@ -186,10 +187,10 @@ public class GameManager : MonoBehaviour
|
||||||
{
|
{
|
||||||
string playerId = SystemInfo.deviceUniqueIdentifier;
|
string playerId = SystemInfo.deviceUniqueIdentifier;
|
||||||
string playerName = "UnityPlayer_" + Random.Range(1000, 9999);
|
string playerName = "UnityPlayer_" + Random.Range(1000, 9999);
|
||||||
|
|
||||||
StartCoroutine(AethexAuth.AuthenticatePlayer(playerId, playerName, OnAuthComplete));
|
StartCoroutine(AethexAuth.AuthenticatePlayer(playerId, playerName, OnAuthComplete));
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnAuthComplete(AethexAuth.AuthResponse response)
|
void OnAuthComplete(AethexAuth.AuthResponse response)
|
||||||
{
|
{
|
||||||
if (response.success)
|
if (response.success)
|
||||||
|
|
@ -231,12 +232,12 @@ public:
|
||||||
int32 ExpiresIn;
|
int32 ExpiresIn;
|
||||||
FString Error;
|
FString Error;
|
||||||
};
|
};
|
||||||
|
|
||||||
static void AuthenticatePlayer(
|
static void AuthenticatePlayer(
|
||||||
const FString& PlayerId,
|
const FString& PlayerId,
|
||||||
const FString& PlayerName,
|
const FString& PlayerName,
|
||||||
TFunction<void(const FAuthResponse&)> OnComplete);
|
TFunction<void(const FAuthResponse&)> OnComplete);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static void OnAuthResponse(
|
static void OnAuthResponse(
|
||||||
FHttpRequestPtr Request,
|
FHttpRequestPtr Request,
|
||||||
|
|
@ -259,30 +260,30 @@ void FAethexAuth::AuthenticatePlayer(
|
||||||
TFunction<void(const FAuthResponse&)> OnComplete)
|
TFunction<void(const FAuthResponse&)> OnComplete)
|
||||||
{
|
{
|
||||||
FHttpModule& HttpModule = FHttpModule::Get();
|
FHttpModule& HttpModule = FHttpModule::Get();
|
||||||
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request =
|
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request =
|
||||||
HttpModule.CreateRequest();
|
HttpModule.CreateRequest();
|
||||||
|
|
||||||
Request->SetURL(TEXT("https://aethex.dev/api/games/game-auth"));
|
Request->SetURL(TEXT("https://aethex.dev/api/games/game-auth"));
|
||||||
Request->SetVerb(TEXT("POST"));
|
Request->SetVerb(TEXT("POST"));
|
||||||
Request->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
|
Request->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
|
||||||
|
|
||||||
// Create JSON body
|
// Create JSON body
|
||||||
TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject());
|
TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject());
|
||||||
JsonObject->SetStringField(TEXT("game"), TEXT("unreal"));
|
JsonObject->SetStringField(TEXT("game"), TEXT("unreal"));
|
||||||
JsonObject->SetStringField(TEXT("player_id"), PlayerId);
|
JsonObject->SetStringField(TEXT("player_id"), PlayerId);
|
||||||
JsonObject->SetStringField(TEXT("player_name"), PlayerName);
|
JsonObject->SetStringField(TEXT("player_name"), PlayerName);
|
||||||
JsonObject->SetStringField(TEXT("platform"), TEXT("PC"));
|
JsonObject->SetStringField(TEXT("platform"), TEXT("PC"));
|
||||||
|
|
||||||
FString OutputString;
|
FString OutputString;
|
||||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString);
|
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString);
|
||||||
FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer);
|
FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer);
|
||||||
|
|
||||||
Request->SetContentAsString(OutputString);
|
Request->SetContentAsString(OutputString);
|
||||||
|
|
||||||
Request->OnProcessRequestComplete().BindStatic(
|
Request->OnProcessRequestComplete().BindStatic(
|
||||||
&FAethexAuth::OnAuthResponse,
|
&FAethexAuth::OnAuthResponse,
|
||||||
OnComplete);
|
OnComplete);
|
||||||
|
|
||||||
Request->ProcessRequest();
|
Request->ProcessRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -293,12 +294,12 @@ void FAethexAuth::OnAuthResponse(
|
||||||
TFunction<void(const FAuthResponse&)> OnComplete)
|
TFunction<void(const FAuthResponse&)> OnComplete)
|
||||||
{
|
{
|
||||||
FAuthResponse AuthResponse;
|
FAuthResponse AuthResponse;
|
||||||
|
|
||||||
if (bWasSuccessful && Response.IsValid())
|
if (bWasSuccessful && Response.IsValid())
|
||||||
{
|
{
|
||||||
TSharedPtr<FJsonObject> JsonObject;
|
TSharedPtr<FJsonObject> JsonObject;
|
||||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());
|
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());
|
||||||
|
|
||||||
if (FJsonSerializer::Deserialize(Reader, JsonObject) && JsonObject.IsValid())
|
if (FJsonSerializer::Deserialize(Reader, JsonObject) && JsonObject.IsValid())
|
||||||
{
|
{
|
||||||
AuthResponse.bSuccess = JsonObject->GetBoolField(TEXT("success"));
|
AuthResponse.bSuccess = JsonObject->GetBoolField(TEXT("success"));
|
||||||
|
|
@ -312,7 +313,7 @@ void FAethexAuth::OnAuthResponse(
|
||||||
{
|
{
|
||||||
AuthResponse.Error = TEXT("Authentication request failed");
|
AuthResponse.Error = TEXT("Authentication request failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
OnComplete(AuthResponse);
|
OnComplete(AuthResponse);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -331,68 +332,68 @@ const API_BASE = "https://aethex.dev/api"
|
||||||
func authenticate_player(player_id: String, player_name: String) -> Dictionary:
|
func authenticate_player(player_id: String, player_name: String) -> Dictionary:
|
||||||
var http_request = HTTPRequest.new()
|
var http_request = HTTPRequest.new()
|
||||||
add_child(http_request)
|
add_child(http_request)
|
||||||
|
|
||||||
var url = API_BASE + "/games/game-auth"
|
var url = API_BASE + "/games/game-auth"
|
||||||
var headers = ["Content-Type: application/json"]
|
var headers = ["Content-Type: application/json"]
|
||||||
|
|
||||||
var body = {
|
var body = {
|
||||||
"game": "godot",
|
"game": "godot",
|
||||||
"player_id": player_id,
|
"player_id": player_id,
|
||||||
"player_name": player_name,
|
"player_name": player_name,
|
||||||
"platform": OS.get_name()
|
"platform": OS.get_name()
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = http_request.request(
|
var response = http_request.request(
|
||||||
url,
|
url,
|
||||||
headers,
|
headers,
|
||||||
HTTPClient.METHOD_POST,
|
HTTPClient.METHOD_POST,
|
||||||
JSON.stringify(body)
|
JSON.stringify(body)
|
||||||
)
|
)
|
||||||
|
|
||||||
if response != OK:
|
if response != OK:
|
||||||
return {"error": "Request failed"}
|
return {"error": "Request failed"}
|
||||||
|
|
||||||
var result = await http_request.request_completed
|
var result = await http_request.request_completed
|
||||||
|
|
||||||
if result[1] != 200:
|
if result[1] != 200:
|
||||||
return {"error": "Authentication failed"}
|
return {"error": "Authentication failed"}
|
||||||
|
|
||||||
var response_data = JSON.parse_string(result[3].get_string_from_utf8())
|
var response_data = JSON.parse_string(result[3].get_string_from_utf8())
|
||||||
http_request.queue_free()
|
http_request.queue_free()
|
||||||
|
|
||||||
return response_data
|
return response_data
|
||||||
|
|
||||||
func verify_token(session_token: String) -> Dictionary:
|
func verify_token(session_token: String) -> Dictionary:
|
||||||
var http_request = HTTPRequest.new()
|
var http_request = HTTPRequest.new()
|
||||||
add_child(http_request)
|
add_child(http_request)
|
||||||
|
|
||||||
var url = API_BASE + "/games/verify-token"
|
var url = API_BASE + "/games/verify-token"
|
||||||
var headers = ["Content-Type: application/json"]
|
var headers = ["Content-Type: application/json"]
|
||||||
|
|
||||||
var body = {
|
var body = {
|
||||||
"session_token": session_token,
|
"session_token": session_token,
|
||||||
"game": "godot"
|
"game": "godot"
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = http_request.request(
|
var response = http_request.request(
|
||||||
url,
|
url,
|
||||||
headers,
|
headers,
|
||||||
HTTPClient.METHOD_POST,
|
HTTPClient.METHOD_POST,
|
||||||
JSON.stringify(body)
|
JSON.stringify(body)
|
||||||
)
|
)
|
||||||
|
|
||||||
var result = await http_request.request_completed
|
var result = await http_request.request_completed
|
||||||
var response_data = JSON.parse_string(result[3].get_string_from_utf8())
|
var response_data = JSON.parse_string(result[3].get_string_from_utf8())
|
||||||
http_request.queue_free()
|
http_request.queue_free()
|
||||||
|
|
||||||
return response_data
|
return response_data
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
var player_id = OS.get_unique_id()
|
var player_id = OS.get_unique_id()
|
||||||
var player_name = "GodotPlayer_" + str(randi_range(1000, 9999))
|
var player_name = "GodotPlayer_" + str(randi_range(1000, 9999))
|
||||||
|
|
||||||
var auth_result = await authenticate_player(player_id, player_name)
|
var auth_result = await authenticate_player(player_id, player_name)
|
||||||
|
|
||||||
if auth_result.has("success") and auth_result["success"]:
|
if auth_result.has("success") and auth_result["success"]:
|
||||||
print("Authenticated as: ", auth_result["username"])
|
print("Authenticated as: ", auth_result["username"])
|
||||||
# Store token
|
# Store token
|
||||||
|
|
@ -413,27 +414,29 @@ func _ready():
|
||||||
Authenticate a game player and create a session.
|
Authenticate a game player and create a session.
|
||||||
|
|
||||||
**Request:**
|
**Request:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"game": "unity|unreal|godot|roblox|custom",
|
"game": "unity|unreal|godot|roblox|custom",
|
||||||
"player_id": "unique-player-id",
|
"player_id": "unique-player-id",
|
||||||
"player_name": "player-display-name",
|
"player_name": "player-display-name",
|
||||||
"device_id": "optional-device-id",
|
"device_id": "optional-device-id",
|
||||||
"platform": "PC|Mobile|Console"
|
"platform": "PC|Mobile|Console"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
"session_token": "token-string",
|
"session_token": "token-string",
|
||||||
"user_id": "uuid",
|
"user_id": "uuid",
|
||||||
"username": "username",
|
"username": "username",
|
||||||
"game": "unity",
|
"game": "unity",
|
||||||
"expires_in": 604800,
|
"expires_in": 604800,
|
||||||
"api_base_url": "https://aethex.dev/api",
|
"api_base_url": "https://aethex.dev/api",
|
||||||
"docs_url": "https://docs.aethex.dev/game-integration"
|
"docs_url": "https://docs.aethex.dev/game-integration"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -442,24 +445,26 @@ Authenticate a game player and create a session.
|
||||||
Verify a game session token and get player data.
|
Verify a game session token and get player data.
|
||||||
|
|
||||||
**Request:**
|
**Request:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"session_token": "token-string",
|
"session_token": "token-string",
|
||||||
"game": "unity"
|
"game": "unity"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"valid": true,
|
"valid": true,
|
||||||
"user_id": "uuid",
|
"user_id": "uuid",
|
||||||
"username": "username",
|
"username": "username",
|
||||||
"email": "user@example.com",
|
"email": "user@example.com",
|
||||||
"full_name": "Player Name",
|
"full_name": "Player Name",
|
||||||
"game": "unity",
|
"game": "unity",
|
||||||
"platform": "PC",
|
"platform": "PC",
|
||||||
"expires_at": "2025-01-15T10:30:00Z"
|
"expires_at": "2025-01-15T10:30:00Z"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -475,6 +480,7 @@ Verify a game session token and get player data.
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
For issues or questions, visit:
|
For issues or questions, visit:
|
||||||
|
|
||||||
- Docs: https://docs.aethex.dev
|
- Docs: https://docs.aethex.dev
|
||||||
- Discord: https://discord.gg/aethex
|
- Discord: https://discord.gg/aethex
|
||||||
- Email: support@aethex.tech
|
- Email: support@aethex.tech
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue