diff --git a/.gitignore b/.gitignore
index 8652c3158..84f2ae291 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,5 @@ resources/images/.DS_Store
resources/.DS_Store
.env
.prettierrc
-.prettierignore
\ No newline at end of file
+.prettierignore
+.DS_Store
\ No newline at end of file
diff --git a/src/client/index.html b/src/client/index.html
index 500ef4551..375c8a83b 100644
--- a/src/client/index.html
+++ b/src/client/index.html
@@ -158,6 +158,15 @@
+
+
diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts
index 0306f0aa8..584acb147 100644
--- a/src/core/configuration/Config.ts
+++ b/src/core/configuration/Config.ts
@@ -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 {
diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts
index 8d3526634..0dba147b0 100644
--- a/src/core/configuration/DefaultConfig.ts
+++ b/src/core/configuration/DefaultConfig.ts
@@ -19,10 +19,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;
}
diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts
index 03edb8b8b..ac0dd5c16 100644
--- a/src/core/configuration/DevConfig.ts
+++ b/src/core/configuration/DevConfig.ts
@@ -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 {
diff --git a/src/core/configuration/PreprodConfig.ts b/src/core/configuration/PreprodConfig.ts
index 387698762..5204ebde8 100644
--- a/src/core/configuration/PreprodConfig.ts
+++ b/src/core/configuration/PreprodConfig.ts
@@ -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";
+ }
+})();
diff --git a/src/core/configuration/ProdConfig.ts b/src/core/configuration/ProdConfig.ts
index 8318a0816..ce3911c48 100644
--- a/src/core/configuration/ProdConfig.ts
+++ b/src/core/configuration/ProdConfig.ts
@@ -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";
+ }
+})();
diff --git a/src/server/Server.ts b/src/server/Server.ts
index f1df564bf..51c983253 100644
--- a/src/server/Server.ts
+++ b/src/server/Server.ts
@@ -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();
diff --git a/webpack.config.js b/webpack.config.js
index c6e879fae..30177ad50 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -138,6 +138,8 @@ export default (env, argv) => {
"/lobby",
"/archive_singleplayer_game",
"/validate-username",
+ "/auth/callback",
+ "/auth/discord",
],
target: "http://localhost:3000",
secure: false,