diff --git a/client/src/App.tsx b/client/src/App.tsx index b2548b6..fd7e7e0 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -21,6 +21,7 @@ import HomePage from "./routes/home"; import Leaderboard from "./routes/leaderboard"; import AdminPanel from "./routes/admin"; import HeaderNav from "./components/header"; +import DiscordCallback from "./routes/discordCallback"; const router = createBrowserRouter( createRoutesFromElements( @@ -35,6 +36,7 @@ const router = createBrowserRouter( } /> } /> } /> + } /> ) ); diff --git a/client/src/routes/discordCallback.tsx b/client/src/routes/discordCallback.tsx new file mode 100644 index 0000000..70c40d5 --- /dev/null +++ b/client/src/routes/discordCallback.tsx @@ -0,0 +1,86 @@ +import { useEffect, useState } from "react"; +import { Container, Title, Loader, Text, Anchor } from "@mantine/core"; + +export default function DiscordCallback() { + const [status, setStatus] = useState("processing"); + const [message, setMessage] = useState("Connecting Discord..."); + + useEffect(() => { + const handleDiscordCallback = async () => { + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + const error = urlParams.get('error'); + + if (error) { + setStatus("error"); + setMessage(`Discord connection failed: ${error}`); + return; + } + + if (!code) { + setStatus("error"); + setMessage("No authorization code received from Discord"); + return; + } + + try { + const response = await fetch('/api/auth/discord/exchange-token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ code }) + }); + + const data = await response.json(); + + if (response.ok && data.success) { + setStatus("success"); + setMessage(`Discord connected: ${data.discord_tag}`); + setTimeout(() => { + window.location.href = '/home'; + }, 2000); + } else { + setStatus("error"); + setMessage(data.error || data.message || "Discord connection failed"); + } + } catch (error) { + setStatus("error"); + setMessage("Network error during Discord connection"); + console.error('Discord error:', error); + } + }; + + handleDiscordCallback(); + }, []); + + return ( + +
+ {status === "processing" && ( + <> + + {message} + + )} + {status === "success" && ( + <> + Success! + {message} + Redirecting... + + )} + {status === "error" && ( + <> + Connection Failed + {message} + + Back to Home + + + )} +
+
+ ); +} diff --git a/client/src/routes/home.tsx b/client/src/routes/home.tsx index 1adfb99..7d1a2bf 100644 --- a/client/src/routes/home.tsx +++ b/client/src/routes/home.tsx @@ -1,6 +1,37 @@ -import { Paper, Text, Title, Container, Anchor } from "@mantine/core"; +import { Paper, Text, Title, Container, Anchor, Button, Group, Badge } from "@mantine/core"; +import { useEffect, useState } from "react"; export default function HomePage() { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Check if user is authenticated and fetch their info + const checkAuth = async () => { + try { + const response = await fetch('/api/auth/whoami', { + credentials: 'include' + }); + if (response.ok) { + const userData = await response.json(); + if (userData.loggedIn) { + setUser(userData); + if (!userData.discord) { + window.location.href = '/api/auth/discord/login'; + return; + } + } + } + } catch (error) { + console.error('Error checking auth:', error); + } finally { + setLoading(false); + } + }; + + checkAuth(); + }, []); + return ( @@ -18,9 +49,37 @@ export default function HomePage() { page. - More questions? + Discord Connection - Visit our helpdesk or email us at + {loading ? ( + Checking connection status... + ) : user && user.loggedIn ? ( + user.discord ? ( + + +
+ + {user.discord} + Connected + + + Mentors can reach you via Discord + +
+
+
+ ) : ( + <> + Connecting Discord... + + ) + ) : ( + Please log in to connect your Discord account. + )} + + More questions? + + Visit our helpdesk or email us at help@hackmit.org diff --git a/server/config.py b/server/config.py index a28306f..ec2a4ea 100644 --- a/server/config.py +++ b/server/config.py @@ -11,7 +11,7 @@ # CORS configuration FRONTEND_URL = os.environ.get("FRONTEND_URL", "http://localhost:6001") BACKEND_URL = os.environ.get("BACKEND_URL", "http://127.0.0.1:3001") -ALLOWED_DOMAINS = [FRONTEND_URL] +ALLOWED_DOMAINS = [BACKEND_URL] SQLALCHEMY_DATABASE_URI = os.environ.get( "SQLALCHEMY_DATABASE_URI", "postgresql://postgres:password@database/qstackdb" @@ -24,10 +24,11 @@ AUTH0_DOMAIN = os.environ.get("AUTH0_DOMAIN") APP_SECRET_KEY = os.environ.get("APP_SECRET_KEY") MENTOR_PASS = os.environ.get("MENTOR_PASS") +DISCORD_CLIENT_ID = os.getenv("DISCORD_CLIENT_ID") +DISCORD_CLIENT_SECRET = os.getenv("DISCORD_CLIENT_SECRET") ENV = os.environ.get("ENVIRONMENT", "development") - AUTH_ADMINS = [ {"name": "HackMIT", "email": "admin@hackmit.org"}, {"name": "HackMIT", "email": "team@hackmit.org"} diff --git a/server/controllers/auth.py b/server/controllers/auth.py index 0f503e1..650ccc3 100644 --- a/server/controllers/auth.py +++ b/server/controllers/auth.py @@ -6,12 +6,15 @@ from server.models import User from server.config import ( FRONTEND_URL, + BACKEND_URL, MENTOR_PASS, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_DOMAIN, AUTH_USERNAME, - AUTH_PASSWORD + AUTH_PASSWORD, + DISCORD_CLIENT_ID, + DISCORD_CLIENT_SECRET ) auth = APIBlueprint("auth", __name__, url_prefix="/auth") @@ -28,6 +31,15 @@ AUTH0_DOMAIN}/.well-known/openid-configuration", ) +oauth.register( + "discord", + client_id=DISCORD_CLIENT_ID, + client_secret=DISCORD_CLIENT_SECRET, + access_token_url="https://discord.com/api/oauth2/token", + authorize_url="https://discord.com/api/oauth2/authorize", + api_base_url="https://discord.com/api/", + client_kwargs={"scope": "identify email"}, +) def auth_required_decorator(roles): """ @@ -96,6 +108,58 @@ def logout(): ) ) +@auth.route("/discord/login") +def discord_login(): + if "user" not in session: + return redirect(FRONTEND_URL + "/api/auth/login") + + return oauth.discord.authorize_redirect( + redirect_uri=FRONTEND_URL + "/auth/discord/callback" + ) + +@auth.route("/discord/exchange-token", methods=["POST"]) +def discord_exchange_token(): + data = request.get_json() + code = data.get("code") + + if not code: + return abort(400, "Missing authorization code") + + # Check if user is logged in via Auth0 first + if "user" not in session: + return {"success": False, "error": "Must be logged in via Auth0 first"} + + try: + # Exchange code for token using the Discord OAuth client + token = oauth.discord.fetch_access_token( + code=code, + redirect_uri=FRONTEND_URL + "/auth/discord/callback" + ) + + # Get Discord user profile + resp = oauth.discord.get("users/@me", token=token) + profile = resp.json() + + # Extract Discord info + discord_tag = f"{profile['username']}#{profile['discriminator']}" + discord_id = profile['id'] + + # Update user in database + email = session["user"]["userinfo"]["email"] + user = User.query.filter_by(email=email).first() + + if not user: + return {"success": False, "error": "User not found"} + + user.discord = discord_tag + db.session.commit() + + return {"success": True, "discord_tag": discord_tag} + + except Exception as e: + return {"success": False, "error": str(e)} + + @auth.route("/whoami") def whoami():