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 c07e0df..0000000 Binary files a/server/sandbox.db-shm and /dev/null differ diff --git a/server/sandbox.db-wal b/server/sandbox.db-wal deleted file mode 100644 index ea5a6b4..0000000 Binary files a/server/sandbox.db-wal and /dev/null differ