Prettier format pending files
This commit is contained in:
parent
ba233408ed
commit
e5aacf6773
25 changed files with 817 additions and 551 deletions
|
|
@ -64,14 +64,13 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|||
|
||||
if (existingLink) {
|
||||
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
|
||||
const { error: linkError } = await supabase
|
||||
.from("discord_links")
|
||||
.insert({
|
||||
const { error: linkError } = await supabase.from("discord_links").insert({
|
||||
discord_id: verification.discord_id,
|
||||
user_id,
|
||||
primary_arm: "labs", // Default to labs
|
||||
|
|
|
|||
|
|
@ -24,7 +24,10 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|||
|
||||
// Verify request is from Discord bot (simple verification)
|
||||
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" });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,11 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|||
const secret = process.env.ROBLOX_SHARED_SECRET || "";
|
||||
|
||||
// Verify signature for security
|
||||
if (signature && secret && !verifyRobloxSignature(payload, signature, secret)) {
|
||||
if (
|
||||
signature &&
|
||||
secret &&
|
||||
!verifyRobloxSignature(payload, signature, secret)
|
||||
) {
|
||||
console.warn("Invalid Roblox signature");
|
||||
return res.status(401).json({ error: "Invalid signature" });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
const { data: sessionData, error: sessionError } = await supabase
|
||||
.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))
|
||||
.single();
|
||||
|
||||
|
|
@ -38,7 +41,9 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|||
|
||||
// Optional: Verify game matches if provided
|
||||
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
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|||
const clientId = process.env.ROBLOX_OAUTH_CLIENT_ID;
|
||||
const clientSecret = process.env.ROBLOX_OAUTH_CLIENT_SECRET;
|
||||
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) {
|
||||
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();
|
||||
|
||||
// Get user info with access token
|
||||
const userResponse = await fetch("https://apis.roblox.com/oauth/v1/userinfo", {
|
||||
const userResponse = await fetch(
|
||||
"https://apis.roblox.com/oauth/v1/userinfo",
|
||||
{
|
||||
headers: { Authorization: `Bearer ${tokenData.access_token}` },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if (!userResponse.ok) {
|
||||
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);
|
||||
|
||||
// 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) {
|
||||
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);
|
||||
|
||||
// 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) {
|
||||
return res.status(401).json({ error: "Invalid token" });
|
||||
}
|
||||
|
|
@ -66,15 +67,16 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|||
.eq("nonce", nonce);
|
||||
|
||||
// Link wallet to existing user
|
||||
const { error: linkError } = await supabase
|
||||
.from("web3_wallets")
|
||||
.insert({
|
||||
const { error: linkError } = await supabase.from("web3_wallets").insert({
|
||||
user_id: userData.user.id,
|
||||
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);
|
||||
return res.status(500).json({ error: "Failed to link wallet" });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,8 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|||
const username = normalizedAddress.substring(2, 10);
|
||||
|
||||
// Create Supabase auth user
|
||||
const { data: authData, error: authError } = await supabase.auth.admin.createUser({
|
||||
const { data: authData, error: authError } =
|
||||
await supabase.auth.admin.createUser({
|
||||
email,
|
||||
password: require("crypto").randomBytes(32).toString("hex"),
|
||||
email_confirm: true,
|
||||
|
|
|
|||
|
|
@ -205,8 +205,14 @@ const App = () => (
|
|||
/>
|
||||
|
||||
{/* Service routes */}
|
||||
<Route path="/game-development" element={<GameDevelopment />} />
|
||||
<Route path="/consulting" element={<DevelopmentConsulting />} />
|
||||
<Route
|
||||
path="/game-development"
|
||||
element={<GameDevelopment />}
|
||||
/>
|
||||
<Route
|
||||
path="/consulting"
|
||||
element={<DevelopmentConsulting />}
|
||||
/>
|
||||
<Route path="/mentorship" element={<MentorshipPrograms />} />
|
||||
<Route path="/engage" element={<Engage />} />
|
||||
<Route
|
||||
|
|
@ -295,8 +301,14 @@ const App = () => (
|
|||
<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/teams"
|
||||
element={<FoundationTeams />}
|
||||
/>
|
||||
<Route
|
||||
path="/community/about"
|
||||
element={<FoundationAbout />}
|
||||
/>
|
||||
<Route
|
||||
path="/community/mentorship"
|
||||
element={<MentorshipRequest />}
|
||||
|
|
@ -317,7 +329,10 @@ const App = () => (
|
|||
|
||||
{/* Informational routes */}
|
||||
<Route path="/wix" element={<Wix />} />
|
||||
<Route path="/wix/case-studies" element={<WixCaseStudies />} />
|
||||
<Route
|
||||
path="/wix/case-studies"
|
||||
element={<WixCaseStudies />}
|
||||
/>
|
||||
<Route path="/wix/faq" element={<WixFaq />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
<Route path="/contact" element={<Contact />} />
|
||||
|
|
|
|||
|
|
@ -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 { CheckCircle2 } from "lucide-react";
|
||||
|
||||
|
|
@ -12,7 +18,8 @@ const REALMS = [
|
|||
{
|
||||
id: "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",
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<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">
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{REALMS.map(realm => (
|
||||
{REALMS.map((realm) => (
|
||||
<div
|
||||
key={realm.id}
|
||||
onClick={() => onSelect(realm.id)}
|
||||
|
|
@ -67,7 +81,9 @@ export default function RealmSelection({ selectedRealm, onSelect, onNext }: Real
|
|||
>
|
||||
<Card
|
||||
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`}
|
||||
>
|
||||
<CardHeader>
|
||||
|
|
@ -81,7 +97,9 @@ export default function RealmSelection({ selectedRealm, onSelect, onNext }: Real
|
|||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="text-sm">{realm.description}</CardDescription>
|
||||
<CardDescription className="text-sm">
|
||||
{realm.description}
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -836,8 +836,14 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||
window.localStorage.removeItem("onboarding_complete");
|
||||
window.localStorage.removeItem("aethex_onboarding_progress_v1");
|
||||
Object.keys(window.localStorage)
|
||||
.filter(key => key.startsWith("sb-") || key.includes("supabase") || key.startsWith("mock_") || key.startsWith("demo_"))
|
||||
.forEach(key => window.localStorage.removeItem(key));
|
||||
.filter(
|
||||
(key) =>
|
||||
key.startsWith("sb-") ||
|
||||
key.includes("supabase") ||
|
||||
key.startsWith("mock_") ||
|
||||
key.startsWith("demo_"),
|
||||
)
|
||||
.forEach((key) => window.localStorage.removeItem(key));
|
||||
console.log("localStorage cleared");
|
||||
} catch (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";
|
||||
|
||||
export interface Web3ContextType {
|
||||
|
|
@ -13,7 +19,9 @@ export interface Web3ContextType {
|
|||
|
||||
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 [isConnected, setIsConnected] = useState(false);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
|
|
@ -100,7 +108,11 @@ export const Web3Provider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||
|
||||
const signMessage = useCallback(
|
||||
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");
|
||||
}
|
||||
|
||||
|
|
@ -144,8 +156,14 @@ export const Web3Provider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||
(window as any).ethereum.on("chainChanged", handleChainChanged);
|
||||
|
||||
return () => {
|
||||
(window as any).ethereum?.removeListener("accountsChanged", handleAccountsChanged);
|
||||
(window as any).ethereum?.removeListener("chainChanged", handleChainChanged);
|
||||
(window as any).ethereum?.removeListener(
|
||||
"accountsChanged",
|
||||
handleAccountsChanged,
|
||||
);
|
||||
(window as any).ethereum?.removeListener(
|
||||
"chainChanged",
|
||||
handleChainChanged,
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -1513,27 +1513,59 @@ export default function Admin() {
|
|||
<CardContent className="space-y-6">
|
||||
<div className="grid gap-4">
|
||||
<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">
|
||||
<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>
|
||||
<div className="mt-4 space-y-3">
|
||||
{[
|
||||
{ realm: "Labs", role: "Labs Creator", members: 234 },
|
||||
{ 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 },
|
||||
{
|
||||
realm: "Labs",
|
||||
role: "Labs Creator",
|
||||
members: 234,
|
||||
},
|
||||
{
|
||||
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) => (
|
||||
<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>
|
||||
<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 className="text-right">
|
||||
<p className="text-lg font-bold text-purple-400">{mapping.members}</p>
|
||||
<p className="text-xs text-gray-500">assigned members</p>
|
||||
<p className="text-lg font-bold text-purple-400">
|
||||
{mapping.members}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
assigned members
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -1542,19 +1574,31 @@ export default function Admin() {
|
|||
</div>
|
||||
|
||||
<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="flex items-center justify-between">
|
||||
<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 className="flex items-center justify-between">
|
||||
<span className="text-gray-400">Servers Connected</span>
|
||||
<span className="font-mono text-purple-400">12 servers</span>
|
||||
<span className="text-gray-400">
|
||||
Servers Connected
|
||||
</span>
|
||||
<span className="font-mono text-purple-400">
|
||||
12 servers
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-400">Linked Accounts</span>
|
||||
<span className="font-mono text-purple-400">1,234 users</span>
|
||||
<span className="text-gray-400">
|
||||
Linked Accounts
|
||||
</span>
|
||||
<span className="font-mono text-purple-400">
|
||||
1,234 users
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@ export default function DiscordVerify() {
|
|||
|
||||
toastSuccess({
|
||||
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
|
||||
|
|
|
|||
|
|
@ -514,7 +514,10 @@ export default function Onboarding() {
|
|||
selectedRealm={data.creatorProfile.primaryArm || ""}
|
||||
onSelect={(realm) =>
|
||||
updateData({
|
||||
creatorProfile: { ...data.creatorProfile, primaryArm: realm },
|
||||
creatorProfile: {
|
||||
...data.creatorProfile,
|
||||
primaryArm: realm,
|
||||
},
|
||||
})
|
||||
}
|
||||
onNext={nextStep}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ export default function RobloxCallback() {
|
|||
const errorData = await response.json();
|
||||
toastError({
|
||||
title: "Authentication failed",
|
||||
description: errorData.error || "Could not authenticate with Roblox",
|
||||
description:
|
||||
errorData.error || "Could not authenticate with Roblox",
|
||||
});
|
||||
navigate("/login");
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -60,7 +60,8 @@ export default function Web3Callback() {
|
|||
if (user && data.success) {
|
||||
toastSuccess({
|
||||
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");
|
||||
return;
|
||||
|
|
@ -93,11 +94,21 @@ export default function Web3Callback() {
|
|||
if (account && !authLoading) {
|
||||
handleWeb3Auth();
|
||||
}
|
||||
}, [account, authLoading, signMessage, user, navigate, toastError, toastSuccess]);
|
||||
}, [
|
||||
account,
|
||||
authLoading,
|
||||
signMessage,
|
||||
user,
|
||||
navigate,
|
||||
toastError,
|
||||
toastSuccess,
|
||||
]);
|
||||
|
||||
return (
|
||||
<LoadingScreen
|
||||
message={isProcessing ? "Verifying your wallet..." : "Connecting wallet..."}
|
||||
message={
|
||||
isProcessing ? "Verifying your wallet..." : "Connecting wallet..."
|
||||
}
|
||||
showProgress={true}
|
||||
duration={5000}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
### Step 1: Prepare the Bot Directory
|
||||
|
||||
Ensure all bot files are committed:
|
||||
|
||||
```
|
||||
code/discord-bot/
|
||||
├── bot.js
|
||||
|
|
@ -53,16 +54,19 @@ NODE_ENV=production
|
|||
In Spaceship Application Settings:
|
||||
|
||||
**Build Command:**
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
**Start Command:**
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
**Root Directory:**
|
||||
|
||||
```
|
||||
code/discord-bot
|
||||
```
|
||||
|
|
@ -80,6 +84,7 @@ code/discord-bot
|
|||
### Step 6: Verify Bot is Online
|
||||
|
||||
Once deployed:
|
||||
|
||||
1. Go to your Discord server
|
||||
2. Type `/verify` - the command autocomplete should appear
|
||||
3. Bot should be online with status "Listening to /verify to link your AeThex account"
|
||||
|
|
@ -87,6 +92,7 @@ Once deployed:
|
|||
## 📡 Discord Bot Endpoints
|
||||
|
||||
The bot will be accessible at:
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
Frontend calls to link Discord accounts:
|
||||
|
||||
- **Endpoint:** `POST /api/discord/link`
|
||||
- **Body:** `{ verification_code, user_id }`
|
||||
- **Response:** `{ success: true, message: "..." }`
|
||||
|
||||
Discord Verify page (`/discord-verify?code=XXX`) will automatically:
|
||||
|
||||
1. Call `/api/discord/link` with the verification code
|
||||
2. Link the Discord ID to the AeThex user account
|
||||
3. Redirect to dashboard on success
|
||||
|
|
@ -108,22 +116,26 @@ Discord Verify page (`/discord-verify?code=XXX`) will automatically:
|
|||
## 🛠️ Debugging
|
||||
|
||||
### Check bot logs on Spaceship:
|
||||
|
||||
- Application → Logs
|
||||
- Filter for "bot.js" or "error"
|
||||
|
||||
### Common issues:
|
||||
|
||||
**"Discord bot not responding to commands"**
|
||||
|
||||
- Check: `DISCORD_BOT_TOKEN` is correct
|
||||
- Check: Bot is added to the Discord server with "applications.commands" scope
|
||||
- Check: Spaceship logs show "✅ Logged in"
|
||||
|
||||
**"Supabase verification fails"**
|
||||
|
||||
- Check: `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE` are correct
|
||||
- Check: `discord_links` and `discord_verifications` tables exist
|
||||
- Run migration: `code/supabase/migrations/20250107_add_discord_integration.sql`
|
||||
|
||||
**"Slash commands not appearing in Discord"**
|
||||
|
||||
- Check: Logs show "✅ Successfully registered X slash commands"
|
||||
- Discord may need 1-2 minutes to sync commands
|
||||
- Try typing `/` in Discord to force refresh
|
||||
|
|
@ -132,12 +144,14 @@ Discord Verify page (`/discord-verify?code=XXX`) will automatically:
|
|||
## 📊 Monitoring
|
||||
|
||||
### Key metrics to monitor:
|
||||
|
||||
- Bot uptime (should be 24/7)
|
||||
- Command usage (in Supabase)
|
||||
- Verification code usage (in Supabase)
|
||||
- Discord role sync success rate
|
||||
|
||||
### View in Admin Dashboard:
|
||||
|
||||
- AeThex Admin Panel → Discord Management tab
|
||||
- Shows:
|
||||
- Bot status
|
||||
|
|
@ -156,6 +170,7 @@ Discord Verify page (`/discord-verify?code=XXX`) will automatically:
|
|||
## 🆘 Support
|
||||
|
||||
For issues:
|
||||
|
||||
1. Check Spaceship logs
|
||||
2. Review `/api/discord/link` endpoint response
|
||||
3. Verify all environment variables are set correctly
|
||||
|
|
@ -177,6 +192,7 @@ Run this migration on your AeThex Supabase:
|
|||
## 🎉 You're All Set!
|
||||
|
||||
Once deployed, users can:
|
||||
|
||||
1. Click "Link Discord" in their profile settings
|
||||
2. Type `/verify` in Discord
|
||||
3. Click the verification link
|
||||
|
|
|
|||
|
|
@ -1,11 +1,20 @@
|
|||
const { Client, GatewayIntentBits, REST, Routes, Collection, EmbedBuilder } = require('discord.js');
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
require('dotenv').config();
|
||||
const {
|
||||
Client,
|
||||
GatewayIntentBits,
|
||||
REST,
|
||||
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
|
||||
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.DirectMessages] });
|
||||
const client = new Client({
|
||||
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.DirectMessages],
|
||||
});
|
||||
|
||||
// Initialize Supabase
|
||||
const supabase = createClient(
|
||||
|
|
@ -17,35 +26,41 @@ const supabase = createClient(
|
|||
client.commands = new Collection();
|
||||
|
||||
// Load commands from commands directory
|
||||
const commandsPath = path.join(__dirname, 'commands');
|
||||
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
||||
const commandsPath = path.join(__dirname, "commands");
|
||||
const commandFiles = fs
|
||||
.readdirSync(commandsPath)
|
||||
.filter((file) => file.endsWith(".js"));
|
||||
|
||||
for (const file of commandFiles) {
|
||||
const filePath = path.join(commandsPath, file);
|
||||
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);
|
||||
console.log(`✅ Loaded command: ${command.data.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Bot ready event
|
||||
client.once('ready', () => {
|
||||
client.once("ready", () => {
|
||||
console.log(`✅ Bot logged in as ${client.user.tag}`);
|
||||
console.log(`📡 Listening in ${client.guilds.cache.size} server(s)`);
|
||||
|
||||
// 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
|
||||
client.on('interactionCreate', async interaction => {
|
||||
client.on("interactionCreate", async (interaction) => {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
const command = client.commands.get(interaction.commandName);
|
||||
|
||||
if (!command) {
|
||||
console.warn(`⚠️ No command matching ${interaction.commandName} was found.`);
|
||||
console.warn(
|
||||
`⚠️ No command matching ${interaction.commandName} was found.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -55,10 +70,10 @@ client.on('interactionCreate', async interaction => {
|
|||
console.error(`❌ Error executing ${interaction.commandName}:`, error);
|
||||
|
||||
const errorEmbed = new EmbedBuilder()
|
||||
.setColor(0xFF0000)
|
||||
.setTitle('❌ Command Error')
|
||||
.setDescription('There was an error while executing this command.')
|
||||
.setFooter({ text: 'Contact support if this persists' });
|
||||
.setColor(0xff0000)
|
||||
.setTitle("❌ Command Error")
|
||||
.setDescription("There was an error while executing this command.")
|
||||
.setFooter({ text: "Contact support if this persists" });
|
||||
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.followUp({ embeds: [errorEmbed], ephemeral: true });
|
||||
|
|
@ -76,7 +91,9 @@ async function registerCommands() {
|
|||
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...`);
|
||||
|
||||
|
|
@ -87,24 +104,24 @@ async function registerCommands() {
|
|||
|
||||
console.log(`✅ Successfully registered ${data.length} slash commands.`);
|
||||
} catch (error) {
|
||||
console.error('❌ Error registering commands:', error);
|
||||
console.error("❌ Error registering commands:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Login and register commands
|
||||
client.login(process.env.DISCORD_BOT_TOKEN);
|
||||
|
||||
client.once('ready', async () => {
|
||||
client.once("ready", async () => {
|
||||
await registerCommands();
|
||||
});
|
||||
|
||||
// Error handling
|
||||
process.on('unhandledRejection', error => {
|
||||
console.error('❌ Unhandled Promise Rejection:', error);
|
||||
process.on("unhandledRejection", (error) => {
|
||||
console.error("❌ Unhandled Promise Rejection:", error);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', error => {
|
||||
console.error('❌ Uncaught Exception:', error);
|
||||
process.on("uncaughtException", (error) => {
|
||||
console.error("❌ Uncaught Exception:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,74 +1,91 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('profile')
|
||||
.setDescription('View your AeThex profile in Discord'),
|
||||
.setName("profile")
|
||||
.setDescription("View your AeThex profile in Discord"),
|
||||
|
||||
async execute(interaction, supabase) {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
try {
|
||||
const { data: link } = await supabase
|
||||
.from('discord_links')
|
||||
.select('user_id, primary_arm')
|
||||
.eq('discord_id', interaction.user.id)
|
||||
.from("discord_links")
|
||||
.select("user_id, primary_arm")
|
||||
.eq("discord_id", interaction.user.id)
|
||||
.single();
|
||||
|
||||
if (!link) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xFF6B6B)
|
||||
.setTitle('❌ Not Linked')
|
||||
.setDescription('You must link your Discord account to AeThex first.\nUse `/verify` to get started.');
|
||||
.setColor(0xff6b6b)
|
||||
.setTitle("❌ Not Linked")
|
||||
.setDescription(
|
||||
"You must link your Discord account to AeThex first.\nUse `/verify` to get started.",
|
||||
);
|
||||
|
||||
return await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const { data: profile } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('*')
|
||||
.eq('id', link.user_id)
|
||||
.from("user_profiles")
|
||||
.select("*")
|
||||
.eq("id", link.user_id)
|
||||
.single();
|
||||
|
||||
if (!profile) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xFF6B6B)
|
||||
.setTitle('❌ Profile Not Found')
|
||||
.setDescription('Your AeThex profile could not be found.');
|
||||
.setColor(0xff6b6b)
|
||||
.setTitle("❌ Profile Not Found")
|
||||
.setDescription("Your AeThex profile could not be found.");
|
||||
|
||||
return await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const armEmojis = {
|
||||
labs: '🧪',
|
||||
gameforge: '🎮',
|
||||
corp: '💼',
|
||||
foundation: '🤝',
|
||||
devlink: '💻',
|
||||
labs: "🧪",
|
||||
gameforge: "🎮",
|
||||
corp: "💼",
|
||||
foundation: "🤝",
|
||||
devlink: "💻",
|
||||
};
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7289DA)
|
||||
.setTitle(`${profile.full_name || 'AeThex User'}'s Profile`)
|
||||
.setThumbnail(profile.avatar_url || 'https://aethex.dev/placeholder.svg')
|
||||
.addFields(
|
||||
{ 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 },
|
||||
.setColor(0x7289da)
|
||||
.setTitle(`${profile.full_name || "AeThex User"}'s Profile`)
|
||||
.setThumbnail(
|
||||
profile.avatar_url || "https://aethex.dev/placeholder.svg",
|
||||
)
|
||||
.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] });
|
||||
} catch (error) {
|
||||
console.error('Profile command error:', error);
|
||||
console.error("Profile command error:", error);
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xFF0000)
|
||||
.setTitle('❌ Error')
|
||||
.setDescription('Failed to fetch profile. Please try again.');
|
||||
.setColor(0xff0000)
|
||||
.setTitle("❌ Error")
|
||||
.setDescription("Failed to fetch profile. Please try again.");
|
||||
|
||||
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 = [
|
||||
{ value: 'labs', label: '🧪 Labs', description: 'Research & Development' },
|
||||
{ value: 'gameforge', label: '🎮 GameForge', 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' },
|
||||
{ value: "labs", label: "🧪 Labs", description: "Research & Development" },
|
||||
{
|
||||
value: "gameforge",
|
||||
label: "🎮 GameForge",
|
||||
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 = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('set-realm')
|
||||
.setDescription('Set your primary AeThex realm/arm'),
|
||||
.setName("set-realm")
|
||||
.setDescription("Set your primary AeThex realm/arm"),
|
||||
|
||||
async execute(interaction, supabase) {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
try {
|
||||
const { data: link } = await supabase
|
||||
.from('discord_links')
|
||||
.select('user_id, primary_arm')
|
||||
.eq('discord_id', interaction.user.id)
|
||||
.from("discord_links")
|
||||
.select("user_id, primary_arm")
|
||||
.eq("discord_id", interaction.user.id)
|
||||
.single();
|
||||
|
||||
if (!link) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xFF6B6B)
|
||||
.setTitle('❌ Not Linked')
|
||||
.setDescription('You must link your Discord account to AeThex first.\nUse `/verify` to get started.');
|
||||
.setColor(0xff6b6b)
|
||||
.setTitle("❌ Not Linked")
|
||||
.setDescription(
|
||||
"You must link your Discord account to AeThex first.\nUse `/verify` to get started.",
|
||||
);
|
||||
|
||||
return await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const select = new StringSelectMenuBuilder()
|
||||
.setCustomId('select_realm')
|
||||
.setPlaceholder('Choose your primary realm')
|
||||
.setCustomId("select_realm")
|
||||
.setPlaceholder("Choose your primary realm")
|
||||
.addOptions(
|
||||
REALMS.map(realm => ({
|
||||
REALMS.map((realm) => ({
|
||||
label: realm.label,
|
||||
description: realm.description,
|
||||
value: realm.value,
|
||||
|
|
@ -47,47 +66,59 @@ module.exports = {
|
|||
const row = new ActionRowBuilder().addComponents(select);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7289DA)
|
||||
.setTitle('⚔️ Choose Your Realm')
|
||||
.setDescription('Select your primary AeThex realm. This determines your main Discord role.')
|
||||
.addFields(
|
||||
{ name: 'Current Realm', value: link.primary_arm || 'Not set' },
|
||||
);
|
||||
.setColor(0x7289da)
|
||||
.setTitle("⚔️ Choose Your Realm")
|
||||
.setDescription(
|
||||
"Select your primary AeThex realm. This determines your main Discord role.",
|
||||
)
|
||||
.addFields({
|
||||
name: "Current Realm",
|
||||
value: link.primary_arm || "Not set",
|
||||
});
|
||||
|
||||
await interaction.editReply({ embeds: [embed], components: [row] });
|
||||
|
||||
const filter = i => i.user.id === interaction.user.id && i.customId === 'select_realm';
|
||||
const collector = interaction.channel.createMessageComponentCollector({ filter, time: 60000 });
|
||||
const filter = (i) =>
|
||||
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];
|
||||
|
||||
await supabase
|
||||
.from('discord_links')
|
||||
.from("discord_links")
|
||||
.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()
|
||||
.setColor(0x00FF00)
|
||||
.setTitle('✅ Realm Set')
|
||||
.setDescription(`Your primary realm is now **${realm.label}**\n\nYou'll be assigned the corresponding Discord role.`);
|
||||
.setColor(0x00ff00)
|
||||
.setTitle("✅ Realm Set")
|
||||
.setDescription(
|
||||
`Your primary realm is now **${realm.label}**\n\nYou'll be assigned the corresponding Discord role.`,
|
||||
);
|
||||
|
||||
await i.update({ embeds: [confirmEmbed], components: [] });
|
||||
});
|
||||
|
||||
collector.on('end', collected => {
|
||||
collector.on("end", (collected) => {
|
||||
if (collected.size === 0) {
|
||||
interaction.editReply({ content: 'Realm selection timed out.', components: [] });
|
||||
interaction.editReply({
|
||||
content: "Realm selection timed out.",
|
||||
components: [],
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Set-realm command error:', error);
|
||||
console.error("Set-realm command error:", error);
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xFF0000)
|
||||
.setTitle('❌ Error')
|
||||
.setDescription('Failed to update realm. Please try again.');
|
||||
.setColor(0xff0000)
|
||||
.setTitle("❌ Error")
|
||||
.setDescription("Failed to update realm. Please try again.");
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,49 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('unlink')
|
||||
.setDescription('Unlink your Discord account from AeThex'),
|
||||
.setName("unlink")
|
||||
.setDescription("Unlink your Discord account from AeThex"),
|
||||
|
||||
async execute(interaction, supabase) {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
try {
|
||||
const { data: link } = await supabase
|
||||
.from('discord_links')
|
||||
.select('*')
|
||||
.eq('discord_id', interaction.user.id)
|
||||
.from("discord_links")
|
||||
.select("*")
|
||||
.eq("discord_id", interaction.user.id)
|
||||
.single();
|
||||
|
||||
if (!link) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xFF6B6B)
|
||||
.setTitle('ℹ️ Not Linked')
|
||||
.setDescription('Your Discord account is not linked to AeThex.');
|
||||
.setColor(0xff6b6b)
|
||||
.setTitle("ℹ️ Not Linked")
|
||||
.setDescription("Your Discord account is not linked to AeThex.");
|
||||
|
||||
return await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
// Delete the link
|
||||
await supabase
|
||||
.from('discord_links')
|
||||
.from("discord_links")
|
||||
.delete()
|
||||
.eq('discord_id', interaction.user.id);
|
||||
.eq("discord_id", interaction.user.id);
|
||||
|
||||
// Remove Discord roles from user
|
||||
const guild = interaction.guild;
|
||||
const member = await guild.members.fetch(interaction.user.id);
|
||||
|
||||
// Find and remove all AeThex-related roles
|
||||
const rolesToRemove = member.roles.cache.filter(role =>
|
||||
role.name.includes('Labs') ||
|
||||
role.name.includes('GameForge') ||
|
||||
role.name.includes('Corp') ||
|
||||
role.name.includes('Foundation') ||
|
||||
role.name.includes('Dev-Link') ||
|
||||
role.name.includes('Premium') ||
|
||||
role.name.includes('Creator')
|
||||
const rolesToRemove = member.roles.cache.filter(
|
||||
(role) =>
|
||||
role.name.includes("Labs") ||
|
||||
role.name.includes("GameForge") ||
|
||||
role.name.includes("Corp") ||
|
||||
role.name.includes("Foundation") ||
|
||||
role.name.includes("Dev-Link") ||
|
||||
role.name.includes("Premium") ||
|
||||
role.name.includes("Creator"),
|
||||
);
|
||||
|
||||
for (const [, role] of rolesToRemove) {
|
||||
|
|
@ -54,17 +55,19 @@ module.exports = {
|
|||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x00FF00)
|
||||
.setTitle('✅ Account Unlinked')
|
||||
.setDescription('Your Discord account has been unlinked from AeThex.\nAll associated roles have been removed.');
|
||||
.setColor(0x00ff00)
|
||||
.setTitle("✅ Account Unlinked")
|
||||
.setDescription(
|
||||
"Your Discord account has been unlinked from AeThex.\nAll associated roles have been removed.",
|
||||
);
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
} catch (error) {
|
||||
console.error('Unlink command error:', error);
|
||||
console.error("Unlink command error:", error);
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xFF0000)
|
||||
.setTitle('❌ Error')
|
||||
.setDescription('Failed to unlink account. Please try again.');
|
||||
.setColor(0xff0000)
|
||||
.setTitle("❌ Error")
|
||||
.setDescription("Failed to unlink account. Please try again.");
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,70 +1,95 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('verify-role')
|
||||
.setDescription('Check your AeThex-assigned Discord roles'),
|
||||
.setName("verify-role")
|
||||
.setDescription("Check your AeThex-assigned Discord roles"),
|
||||
|
||||
async execute(interaction, supabase) {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
try {
|
||||
const { data: link } = await supabase
|
||||
.from('discord_links')
|
||||
.select('user_id, primary_arm')
|
||||
.eq('discord_id', interaction.user.id)
|
||||
.from("discord_links")
|
||||
.select("user_id, primary_arm")
|
||||
.eq("discord_id", interaction.user.id)
|
||||
.single();
|
||||
|
||||
if (!link) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xFF6B6B)
|
||||
.setTitle('❌ Not Linked')
|
||||
.setDescription('You must link your Discord account to AeThex first.\nUse `/verify` to get started.');
|
||||
.setColor(0xff6b6b)
|
||||
.setTitle("❌ Not Linked")
|
||||
.setDescription(
|
||||
"You must link your Discord account to AeThex first.\nUse `/verify` to get started.",
|
||||
);
|
||||
|
||||
return await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const { data: profile } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('user_type')
|
||||
.eq('id', link.user_id)
|
||||
.from("user_profiles")
|
||||
.select("user_type")
|
||||
.eq("id", link.user_id)
|
||||
.single();
|
||||
|
||||
const { data: mappings } = await supabase
|
||||
.from('discord_role_mappings')
|
||||
.select('discord_role')
|
||||
.eq('arm', link.primary_arm)
|
||||
.eq('user_type', profile?.user_type || 'community_member');
|
||||
.from("discord_role_mappings")
|
||||
.select("discord_role")
|
||||
.eq("arm", link.primary_arm)
|
||||
.eq("user_type", profile?.user_type || "community_member");
|
||||
|
||||
const member = await interaction.guild.members.fetch(interaction.user.id);
|
||||
const aethexRoles = member.roles.cache.filter(role =>
|
||||
role.name.includes('Labs') ||
|
||||
role.name.includes('GameForge') ||
|
||||
role.name.includes('Corp') ||
|
||||
role.name.includes('Foundation') ||
|
||||
role.name.includes('Dev-Link') ||
|
||||
role.name.includes('Premium') ||
|
||||
role.name.includes('Creator')
|
||||
const aethexRoles = member.roles.cache.filter(
|
||||
(role) =>
|
||||
role.name.includes("Labs") ||
|
||||
role.name.includes("GameForge") ||
|
||||
role.name.includes("Corp") ||
|
||||
role.name.includes("Foundation") ||
|
||||
role.name.includes("Dev-Link") ||
|
||||
role.name.includes("Premium") ||
|
||||
role.name.includes("Creator"),
|
||||
);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7289DA)
|
||||
.setTitle('🔐 Your AeThex Roles')
|
||||
.setColor(0x7289da)
|
||||
.setTitle("🔐 Your AeThex Roles")
|
||||
.addFields(
|
||||
{ name: '⚔️ Primary Realm', value: link.primary_arm || 'Not set', 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' },
|
||||
{
|
||||
name: "⚔️ Primary Realm",
|
||||
value: link.primary_arm || "Not set",
|
||||
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] });
|
||||
} catch (error) {
|
||||
console.error('Verify-role command error:', error);
|
||||
console.error("Verify-role command error:", error);
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xFF0000)
|
||||
.setTitle('❌ Error')
|
||||
.setDescription('Failed to verify roles. Please try again.');
|
||||
.setColor(0xff0000)
|
||||
.setTitle("❌ Error")
|
||||
.setDescription("Failed to verify roles. Please try again.");
|
||||
|
||||
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 = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('verify')
|
||||
.setDescription('Link your Discord account to your AeThex account'),
|
||||
.setName("verify")
|
||||
.setDescription("Link your Discord account to your AeThex account"),
|
||||
|
||||
async execute(interaction, supabase) {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
try {
|
||||
const { data: existingLink } = await supabase
|
||||
.from('discord_links')
|
||||
.select('*')
|
||||
.eq('discord_id', interaction.user.id)
|
||||
.from("discord_links")
|
||||
.select("*")
|
||||
.eq("discord_id", interaction.user.id)
|
||||
.single();
|
||||
|
||||
if (existingLink) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x00FF00)
|
||||
.setTitle('✅ Already Linked')
|
||||
.setDescription(`Your Discord account is already linked to AeThex (User ID: ${existingLink.user_id})`);
|
||||
.setColor(0x00ff00)
|
||||
.setTitle("✅ Already Linked")
|
||||
.setDescription(
|
||||
`Your Discord account is already linked to AeThex (User ID: ${existingLink.user_id})`,
|
||||
);
|
||||
|
||||
return await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Store verification code
|
||||
await supabase.from('discord_verifications').insert({
|
||||
await supabase.from("discord_verifications").insert({
|
||||
discord_id: interaction.user.id,
|
||||
verification_code: verificationCode,
|
||||
expires_at: expiresAt.toISOString(),
|
||||
|
|
@ -38,29 +49,33 @@ module.exports = {
|
|||
const verifyUrl = `https://aethex.dev/discord-verify?code=${verificationCode}`;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7289DA)
|
||||
.setTitle('🔗 Link Your AeThex Account')
|
||||
.setDescription('Click the button below to link your Discord account to AeThex.')
|
||||
.addFields(
|
||||
{ name: '⏱️ Expires In', value: '15 minutes' },
|
||||
{ name: '📝 Verification Code', value: `\`${verificationCode}\`` },
|
||||
.setColor(0x7289da)
|
||||
.setTitle("🔗 Link Your AeThex Account")
|
||||
.setDescription(
|
||||
"Click the button below to link your Discord account to AeThex.",
|
||||
)
|
||||
.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(
|
||||
new ButtonBuilder()
|
||||
.setLabel('Link Account')
|
||||
.setLabel("Link Account")
|
||||
.setStyle(ButtonStyle.Link)
|
||||
.setURL(verifyUrl),
|
||||
);
|
||||
|
||||
await interaction.editReply({ embeds: [embed], components: [row] });
|
||||
} catch (error) {
|
||||
console.error('Verify command error:', error);
|
||||
console.error("Verify command error:", error);
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xFF0000)
|
||||
.setTitle('❌ Error')
|
||||
.setDescription('Failed to generate verification code. Please try again.');
|
||||
.setColor(0xff0000)
|
||||
.setTitle("❌ Error")
|
||||
.setDescription(
|
||||
"Failed to generate verification code. Please try again.",
|
||||
);
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ This guide covers how to integrate AeThex authentication with game engines: Robl
|
|||
## Overview
|
||||
|
||||
AeThex provides a unified authentication system for games to:
|
||||
|
||||
- Authenticate players across different platforms
|
||||
- Link player accounts to Roblox, Ethereum wallets, and other providers
|
||||
- Store player profiles and achievements
|
||||
|
|
@ -413,6 +414,7 @@ func _ready():
|
|||
Authenticate a game player and create a session.
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{
|
||||
"game": "unity|unreal|godot|roblox|custom",
|
||||
|
|
@ -424,6 +426,7 @@ Authenticate a game player and create a session.
|
|||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
|
|
@ -442,6 +445,7 @@ Authenticate a game player and create a session.
|
|||
Verify a game session token and get player data.
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{
|
||||
"session_token": "token-string",
|
||||
|
|
@ -450,6 +454,7 @@ Verify a game session token and get player data.
|
|||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
|
|
@ -475,6 +480,7 @@ Verify a game session token and get player data.
|
|||
## Support
|
||||
|
||||
For issues or questions, visit:
|
||||
|
||||
- Docs: https://docs.aethex.dev
|
||||
- Discord: https://discord.gg/aethex
|
||||
- Email: support@aethex.tech
|
||||
|
|
|
|||
Loading…
Reference in a new issue