Move Discord OAuth to git subdomain auth service at /var/www/git/
This commit is contained in:
parent
2abdb02640
commit
e9ab450772
2
.gitignore
vendored
2
.gitignore
vendored
@ -10,3 +10,5 @@ dist/
|
|||||||
.tmp/
|
.tmp/
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
|||||||
13
AGENTS.md
Normal file
13
AGENTS.md
Normal file
@ -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/`.
|
||||||
@ -66,7 +66,7 @@ export const fragments = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function getDiscordAuthUrl(next = '/') {
|
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 = {
|
export const ai = {
|
||||||
|
|||||||
@ -1,18 +1,11 @@
|
|||||||
import dotenv from 'dotenv';
|
|
||||||
import path from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import authRoutes from './routes/auth.js';
|
import authRoutes from './routes/auth.js';
|
||||||
import discordRoutes from './routes/discord.js';
|
|
||||||
import characterRoutes from './routes/characters.js';
|
import characterRoutes from './routes/characters.js';
|
||||||
import lorebookRoutes from './routes/lorebooks.js';
|
import lorebookRoutes from './routes/lorebooks.js';
|
||||||
import fragmentRoutes from './routes/fragments.js';
|
import fragmentRoutes from './routes/fragments.js';
|
||||||
import aiRoutes from './routes/ai.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 app = express();
|
||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 3001;
|
||||||
|
|
||||||
@ -20,7 +13,6 @@ app.use(cors());
|
|||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/auth', discordRoutes);
|
|
||||||
app.use('/api/characters', characterRoutes);
|
app.use('/api/characters', characterRoutes);
|
||||||
app.use('/api/lorebooks', lorebookRoutes);
|
app.use('/api/lorebooks', lorebookRoutes);
|
||||||
app.use('/api/fragments', fragmentRoutes);
|
app.use('/api/fragments', fragmentRoutes);
|
||||||
|
|||||||
@ -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;
|
|
||||||
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user