create discord login button & flow

This commit is contained in:
Evan
2025-02-13 20:21:23 -08:00
parent 42e49a14af
commit f5ad149f32
9 changed files with 175 additions and 24 deletions
+2 -1
View File
@@ -6,4 +6,5 @@ resources/images/.DS_Store
resources/.DS_Store
.env
.prettierrc
.prettierignore
.prettierignore
.DS_Store
+9
View File
@@ -158,6 +158,15 @@
<username-input class="w-full"></username-input>
</div>
<div class="max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto mt-4">
<a
href="/auth/discord"
class="w-full bg-[#5865F2] hover:bg-[#4752C4] text-white p-3 sm:p-4 lg:p-5 font-medium text-lg sm:text-xl lg:text-2xl rounded-lg border-none cursor-pointer transition-colors duration-300 flex justify-center"
>
Login with Discord
</a>
</div>
<div class="max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto p-2">
<public-lobby class="w-full"></public-lobby>
</div>
+3
View File
@@ -24,6 +24,7 @@ import { UserSettings } from "../game/UserSettings";
export enum GameEnv {
Dev,
Preprod,
Prod,
}
export function getConfig(
@@ -64,6 +65,8 @@ export interface ServerConfig {
turnIntervalMs(): number;
gameCreationRate(): number;
lobbyLifetime(): number;
discordRedirectURI(): string;
env(): GameEnv;
}
export interface Config {
+3 -1
View File
@@ -17,10 +17,12 @@ import { PlayerView } from "../game/GameView";
import { UserSettings } from "../game/UserSettings";
import { GameConfig } from "../Schemas";
import { assertNever, within } from "../Util";
import { Config, ServerConfig, Theme } from "./Config";
import { Config, GameEnv, ServerConfig, Theme } from "./Config";
import { pastelTheme } from "./PastelTheme";
export abstract class DefaultServerConfig implements ServerConfig {
abstract env(): GameEnv;
abstract discordRedirectURI(): string;
turnIntervalMs(): number {
return 100;
}
+9 -1
View File
@@ -1,16 +1,24 @@
import { GameType, Player, PlayerInfo, UnitInfo, UnitType } from "../game/Game";
import { UserSettings } from "../game/UserSettings";
import { GameConfig } from "../Schemas";
import { ServerConfig } from "./Config";
import { GameEnv, ServerConfig } from "./Config";
import { DefaultConfig, DefaultServerConfig } from "./DefaultConfig";
export class DevServerConfig extends DefaultServerConfig {
env(): GameEnv {
return GameEnv.Dev;
}
gameCreationRate(): number {
return 10 * 1000;
}
lobbyLifetime(): number {
return 10 * 1000;
}
discordRedirectURI(): string {
return "http://localhost:3000/auth/callback";
}
}
export class DevConfig extends DefaultConfig {
+9 -1
View File
@@ -1,3 +1,11 @@
import { GameEnv } from "./Config";
import { DefaultConfig, DefaultServerConfig } from "./DefaultConfig";
export const preprodConfig = new (class extends DefaultServerConfig {})();
export const preprodConfig = new (class extends DefaultServerConfig {
env(): GameEnv {
return GameEnv.Preprod;
}
discordRedirectURI(): string {
return "https://openfront.dev/auth/callback";
}
})();
+9 -1
View File
@@ -1,3 +1,11 @@
import { GameEnv } from "./Config";
import { DefaultConfig, DefaultServerConfig } from "./DefaultConfig";
export const prodConfig = new (class extends DefaultServerConfig {})();
export const prodConfig = new (class extends DefaultServerConfig {
env(): GameEnv {
return GameEnv.Prod;
}
discordRedirectURI(): string {
return "https://openfront.io/auth/callback";
}
})();
+129 -19
View File
@@ -11,7 +11,11 @@ import {
GameRecordSchema,
LogSeverity,
} from "../core/Schemas";
import { getConfig, getServerConfig } from "../core/configuration/Config";
import {
GameEnv,
getConfig,
getServerConfig,
} from "../core/configuration/Config";
import { slog } from "./StructuredLog";
import { Client } from "./Client";
import { GamePhase, GameServer } from "./GameServer";
@@ -22,7 +26,10 @@ import {
validateUsername,
} from "../core/validations/username";
import { Request, Response } from "express";
import { SecretManagerServiceClient } from "@google-cloud/secret-manager";
import dotenv from "dotenv";
import crypto from "crypto";
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -30,21 +37,90 @@ const app = express();
const server = http.createServer(app);
const wss = new WebSocketServer({ server });
const serverConfig = getServerConfig();
// Initialize Secret Manager
const secretManager = new SecretManagerServiceClient();
// Discord OAuth Configuration (will be populated from secrets)
let DISCORD_CLIENT_ID: string;
let DISCORD_CLIENT_SECRET: string;
// Serve static files from the 'out' directory
app.use(express.static(path.join(__dirname, "../../out")));
app.use(express.json());
const gm = new GameManager(getServerConfig());
const bot = new DiscordBot();
try {
await bot.start();
} catch (error) {
console.error("Failed to start bot:", error);
}
const gm = new GameManager(serverConfig);
let lobbiesString = "";
// Discord OAuth callback endpoint
app.get("/auth/callback", async (req: Request, res: Response) => {
const { code } = req.query;
if (!code) {
return res.status(400).send("No code provided");
}
try {
// Exchange code for access token
const tokenResponse = await fetch("https://discord.com/api/oauth2/token", {
method: "POST",
body: new URLSearchParams({
client_id: DISCORD_CLIENT_ID!,
client_secret: DISCORD_CLIENT_SECRET!,
code: code as string,
grant_type: "authorization_code",
redirect_uri: serverConfig.discordRedirectURI(),
}),
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
});
if (!tokenResponse.ok) {
throw new Error("Failed to get access token");
}
const tokenData = await tokenResponse.json();
// Get user information
const userResponse = await fetch("https://discord.com/api/users/@me", {
headers: {
Authorization: `Bearer ${tokenData.access_token}`,
},
});
if (!userResponse.ok) {
throw new Error("Failed to get user information");
}
const userData = await userResponse.json();
const sessionToken = crypto.randomBytes(32).toString("hex");
// TODO: store userData and sessionToken in database.
res.cookie("session", sessionToken, {
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days in milliseconds
});
res.redirect(`/`);
} catch (error) {
console.error("Auth error:", error);
res.status(500).send("Authentication failed");
}
});
app.get("/auth/discord", (req: Request, res: Response) => {
console.log("Redirecting to Discord OAuth...");
const redirectUri = serverConfig.discordRedirectURI();
const authorizeUrl = `https://discord.com/api/oauth2/authorize?client_id=${DISCORD_CLIENT_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=identify`;
console.log("Auth URL:", authorizeUrl);
res.redirect(authorizeUrl);
});
// New GET endpoint to list lobbies
app.get("/lobbies", (req: Request, res: Response) => {
res.send(lobbiesString);
@@ -61,7 +137,7 @@ app.post("/private_lobby", (req, res) => {
app.post("/archive_singleplayer_game", (req, res) => {
try {
const gameRecord: GameRecord = req.body;
const clientIP = req.ip || req.socket.remoteAddress || "unknown"; // Added this line
const clientIP = req.ip || req.socket.remoteAddress || "unknown";
if (!gameRecord) {
console.log("game record not found in request");
@@ -144,7 +220,7 @@ wss.on("connection", (ws, req) => {
if (clientMsg.type == "join") {
const forwarded = req.headers["x-forwarded-for"];
let ip = Array.isArray(forwarded)
? forwarded[0] // Get the first IP if it's an array
? forwarded[0]
: forwarded || req.socket.remoteAddress;
if (Array.isArray(ip)) {
ip = ip[0];
@@ -190,9 +266,18 @@ wss.on("connection", (ws, req) => {
});
});
function runGame() {
function startServer() {
setInterval(() => tick(), 1000);
setInterval(() => updateLobbies(), 100);
initializeSecrets();
const PORT = process.env.PORT || 3000;
console.log(`Server will try to run on http://localhost:${PORT}`);
server.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
}
function tick() {
@@ -214,11 +299,36 @@ function updateLobbies() {
});
}
const PORT = process.env.PORT || 3000;
console.log(`Server will try to run on http://localhost:${PORT}`);
// Initialize secrets and start server
async function initializeSecrets() {
try {
DISCORD_CLIENT_ID = await getSecret(
"DISCORD_CLIENT_ID",
serverConfig.env(),
);
DISCORD_CLIENT_SECRET = await getSecret(
"DISCORD_CLIENT_SECRET",
serverConfig.env(),
);
server.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
if (!DISCORD_CLIENT_ID || !DISCORD_CLIENT_SECRET) {
throw new Error("Failed to load Discord secrets");
}
} catch (error) {
console.error("Failed to initialize secrets:", error);
process.exit(1);
}
}
runGame();
async function getSecret(secretName: string, ge: GameEnv) {
if (ge == GameEnv.Dev) {
console.log(`loading secret ${secretName} from environment variable`);
return process.env[secretName]; // This is how you access env vars dynamically
}
console.log(`loading secret ${secretName} from Google secrets manager`);
const name = `projects/openfrontio/secrets/${secretName}/versions/latest`;
const [version] = await secretManager.accessSecretVersion({ name });
return version.payload?.data?.toString();
}
startServer();
+2
View File
@@ -138,6 +138,8 @@ export default (env, argv) => {
"/lobby",
"/archive_singleplayer_game",
"/validate-username",
"/auth/callback",
"/auth/discord",
],
target: "http://localhost:3000",
secure: false,