From e9ab4507727a18a9e3742aca96cb4ee348122dcc Mon Sep 17 00:00:00 2001 From: The Howling Whispers Date: Tue, 30 Jun 2026 17:16:04 +0200 Subject: [PATCH] Move Discord OAuth to git subdomain auth service at /var/www/git/ --- .gitignore | 2 + AGENTS.md | 13 +++++ client/src/services/api.js | 2 +- server/index.js | 8 --- server/routes/discord.js | 100 ------------------------------------- server/sandbox.db-shm | Bin 32768 -> 0 bytes server/sandbox.db-wal | Bin 164832 -> 0 bytes 7 files changed, 16 insertions(+), 109 deletions(-) create mode 100644 AGENTS.md delete mode 100644 server/routes/discord.js delete mode 100644 server/sandbox.db-shm delete mode 100644 server/sandbox.db-wal diff --git a/.gitignore b/.gitignore index a422975..7a7a07b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ dist/ .tmp/ *.log .DS_Store +*.db-shm +*.db-wal diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6aa27d4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,13 @@ +# Project structure conventions + +## Web projects live under /var/www/ +Each web project gets its own subdirectory under `/var/www/` named after its subdomain: + +| Subdomain | Path | +|---|---| +| sandbox.thehowlingwhispers.com | `/var/www/sandbox/` | +| rp.thehowlingwhispers.com | `/var/www/rp/` | +| play.thehowlingwhispers.com | `/var/www/play/` | +| git.thehowlingwhispers.com | `/var/www/git/` | + +Gitea binary stays at `/usr/local/bin/gitea`, config at `/etc/gitea/app.ini`, data at `/var/lib/gitea/`. diff --git a/client/src/services/api.js b/client/src/services/api.js index d070a9e..c59f51f 100644 --- a/client/src/services/api.js +++ b/client/src/services/api.js @@ -66,7 +66,7 @@ export const fragments = { }; export function getDiscordAuthUrl(next = '/') { - return `${API_ORIGIN}/auth/discord?next=${encodeURIComponent(next)}`; + return `https://git.thehowlingwhispers.com/auth/discord?next=${encodeURIComponent(next)}`; } export const ai = { diff --git a/server/index.js b/server/index.js index 9a5b802..ab29a8b 100644 --- a/server/index.js +++ b/server/index.js @@ -1,18 +1,11 @@ -import dotenv from 'dotenv'; -import path from 'path'; -import { fileURLToPath } from 'url'; import express from 'express'; import cors from 'cors'; import authRoutes from './routes/auth.js'; -import discordRoutes from './routes/discord.js'; import characterRoutes from './routes/characters.js'; import lorebookRoutes from './routes/lorebooks.js'; import fragmentRoutes from './routes/fragments.js'; import aiRoutes from './routes/ai.js'; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -dotenv.config({ path: path.resolve(__dirname, '../.env') }); - const app = express(); const PORT = process.env.PORT || 3001; @@ -20,7 +13,6 @@ app.use(cors()); app.use(express.json()); app.use('/api/auth', authRoutes); -app.use('/auth', discordRoutes); app.use('/api/characters', characterRoutes); app.use('/api/lorebooks', lorebookRoutes); app.use('/api/fragments', fragmentRoutes); diff --git a/server/routes/discord.js b/server/routes/discord.js deleted file mode 100644 index f3cb148..0000000 --- a/server/routes/discord.js +++ /dev/null @@ -1,100 +0,0 @@ -import { Router } from 'express'; -import { v4 as uuidv4 } from 'uuid'; -import db from '../db.js'; -import { generateToken } from '../middleware/auth.js'; - -const router = Router(); - -const DISCORD_API = 'https://discord.com/api'; - -const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID; -const DISCORD_CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET; -const DISCORD_REDIRECT_URI = process.env.DISCORD_REDIRECT_URI; - -const WHITELIST = new Set([ - '1207017997173137481' -]); - -function getRedirectOrigin() { - return process.env.FRONTEND_URL || 'http://localhost:3000'; -} - -router.get('/discord', (req, res) => { - const next = req.query.next || '/'; - const safeNext = next.startsWith('/') && !next.startsWith('//') ? next : '/'; - const params = new URLSearchParams({ - client_id: DISCORD_CLIENT_ID, - redirect_uri: DISCORD_REDIRECT_URI, - response_type: 'code', - scope: 'identify', - state: safeNext, - }); - res.redirect(`${DISCORD_API}/oauth2/authorize?${params.toString()}`); -}); - -router.get('/discord/callback', async (req, res) => { - const { code, state, error } = req.query; - - if (error) { - return res.redirect(`${getRedirectOrigin()}/login?error=${encodeURIComponent(error)}`); - } - if (!code || !state) { - return res.redirect(`${getRedirectOrigin()}/login?error=missing_code_or_state`); - } - - try { - const tokenRes = await fetch(`${DISCORD_API}/oauth2/token`, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ - client_id: DISCORD_CLIENT_ID, - client_secret: DISCORD_CLIENT_SECRET, - grant_type: 'authorization_code', - code, - redirect_uri: DISCORD_REDIRECT_URI, - }).toString(), - }); - if (!tokenRes.ok) { - const text = await tokenRes.text(); - throw new Error(`Discord token exchange failed: ${tokenRes.status} ${text}`); - } - const token = await tokenRes.json(); - - const userRes = await fetch(`${DISCORD_API}/users/@me`, { - headers: { Authorization: `Bearer ${token.access_token}` }, - }); - if (!userRes.ok) { - const text = await userRes.text(); - throw new Error(`Discord user fetch failed: ${userRes.status} ${text}`); - } - const discordUser = await userRes.json(); - - if (!WHITELIST.has(discordUser.id)) { - return res.redirect(`${getRedirectOrigin()}/login?error=not_allowed`); - } - - const existing = db.prepare('SELECT id FROM users WHERE discord_id = ?').get(discordUser.id); - let userId; - if (existing) { - userId = existing.id; - db.prepare( - "UPDATE users SET username = ?, global_name = ?, avatar = ?, updated_at = datetime('now') WHERE id = ?" - ).run(discordUser.username, discordUser.global_name, discordUser.avatar, userId); - } else { - userId = uuidv4(); - db.prepare( - 'INSERT INTO users (id, username, global_name, avatar, discord_id) VALUES (?, ?, ?, ?, ?)' - ).run(userId, discordUser.username, discordUser.global_name, discordUser.avatar, discordUser.id); - } - - const jwt = generateToken(userId); - - const redirectTo = state.startsWith('/') ? state : '/'; - res.redirect(`${getRedirectOrigin()}${redirectTo}?token=${jwt}`); - } catch (e) { - const msg = e instanceof Error ? e.message : 'unknown error'; - res.redirect(`${getRedirectOrigin()}/login?error=${encodeURIComponent(msg.slice(0, 200))}`); - } -}); - -export default router; diff --git a/server/sandbox.db-shm b/server/sandbox.db-shm deleted file mode 100644 index c07e0dfa63176ca7b19e16138c293f7bda015454..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI*J4(Yr5XbRfUgoLB81?V_{Q&U*9zhH747Ogw!cIJcg@vU@u(I+F;sxyNbT(`x zjfjwyh4~IFv;0YBXFdnmEnbflqHL=qbq_4h+PXY>dAhkiygj~p?q8g~-M!yGJbs*? zsrcjXt<)ew-OrJTpS2S=cJ`o$ish=Unyv6{!d5-*8;fgb zU!tjw-5Al+oQkp6ryv6P3Y4|1&3xOVGXxR}RJ5Y4g!L(nK)wQ1t!g{p_UH_OgaS3K zecwEn@EA%XkfA_b>)Oq*FIqw%y+A`7+Dm^7Cm@iaKuh}>c124F{D(kW;T~!N0gnRV zj&uS6p90}!00IG@0^uzT0s)@_;T0GH0iOcl-6H}4p8|cKV>2}ZCNPfz0&WEs+)mEm Z2rNQ?fLnnjx05qC0tg_000O@Wd;#P7EKdLc diff --git a/server/sandbox.db-wal b/server/sandbox.db-wal deleted file mode 100644 index ea5a6b4d8a1a9c5236f120ed9b34079f21628c84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 164832 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