feat: add Steam OpenID login
- Add Steam OpenID 2.0 authentication routes (login + callback) - Enable Steam button in LoginModal (was placeholder) - Unified user ID system: getUserId() supports Discord, Steam, Admin - Update soundboard user-sound endpoints for Steam users - UserSettings now works for both Discord and Steam providers - Steam hover uses brand color #66c0f4 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
aa998c9b44
commit
d135aab6dc
6 changed files with 162 additions and 38 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
// ──────────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
// Discord OAuth2 Authentication + Unified Session Management
|
// Unified Authentication: Discord OAuth2, Steam OpenID 2.0, Admin
|
||||||
// ──────────────────────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────────────────────
|
||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
import type express from 'express';
|
import type express from 'express';
|
||||||
|
|
@ -10,6 +10,7 @@ const DISCORD_CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET ?? '';
|
||||||
const DISCORD_API = 'https://discord.com/api/v10';
|
const DISCORD_API = 'https://discord.com/api/v10';
|
||||||
const DISCORD_AUTH_URL = 'https://discord.com/oauth2/authorize';
|
const DISCORD_AUTH_URL = 'https://discord.com/oauth2/authorize';
|
||||||
const DISCORD_TOKEN_URL = `${DISCORD_API}/oauth2/token`;
|
const DISCORD_TOKEN_URL = `${DISCORD_API}/oauth2/token`;
|
||||||
|
const STEAM_API_KEY = process.env.STEAM_API_KEY ?? '';
|
||||||
const SESSION_MAX_AGE = 30 * 24 * 3600; // 30 days in seconds
|
const SESSION_MAX_AGE = 30 * 24 * 3600; // 30 days in seconds
|
||||||
const ADMIN_MAX_AGE = 7 * 24 * 3600; // 7 days in seconds
|
const ADMIN_MAX_AGE = 7 * 24 * 3600; // 7 days in seconds
|
||||||
|
|
||||||
|
|
@ -23,8 +24,9 @@ export interface DiscordUser {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserSession {
|
export interface UserSession {
|
||||||
provider: 'discord' | 'admin';
|
provider: 'discord' | 'steam' | 'admin';
|
||||||
discordId?: string;
|
discordId?: string;
|
||||||
|
steamId?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
avatar?: string | null;
|
avatar?: string | null;
|
||||||
globalName?: string | null;
|
globalName?: string | null;
|
||||||
|
|
@ -32,6 +34,14 @@ export interface UserSession {
|
||||||
exp: number;
|
exp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns the generic user ID regardless of provider (discordId, steam:steamId, or 'admin') */
|
||||||
|
export function getUserId(session: UserSession): string | null {
|
||||||
|
if (session.discordId) return session.discordId;
|
||||||
|
if (session.steamId) return `steam:${session.steamId}`;
|
||||||
|
if (session.provider === 'admin') return 'admin';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Helpers ──
|
// ── Helpers ──
|
||||||
function b64url(input: Buffer | string): string {
|
function b64url(input: Buffer | string): string {
|
||||||
return Buffer.from(input).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
return Buffer.from(input).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
||||||
|
|
@ -135,6 +145,55 @@ async function fetchDiscordUser(accessToken: string): Promise<DiscordUser> {
|
||||||
return await res.json() as DiscordUser;
|
return await res.json() as DiscordUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Steam OpenID 2.0 ──
|
||||||
|
export function isSteamConfigured(): boolean {
|
||||||
|
return !!STEAM_API_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSteamReturnUrl(req: express.Request): string {
|
||||||
|
const publicUrl = process.env.PUBLIC_URL ?? '';
|
||||||
|
if (publicUrl) return `${publicUrl.replace(/\/$/, '')}/api/auth/steam/callback`;
|
||||||
|
const proto = req.headers['x-forwarded-proto'] || req.protocol || 'http';
|
||||||
|
const host = req.headers['x-forwarded-host'] || req.headers.host || 'localhost';
|
||||||
|
return `${proto}://${host}/api/auth/steam/callback`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSteamRealm(req: express.Request): string {
|
||||||
|
const publicUrl = process.env.PUBLIC_URL ?? '';
|
||||||
|
if (publicUrl) return publicUrl.replace(/\/$/, '');
|
||||||
|
const proto = req.headers['x-forwarded-proto'] || req.protocol || 'http';
|
||||||
|
const host = req.headers['x-forwarded-host'] || req.headers.host || 'localhost';
|
||||||
|
return `${proto}://${host}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifySteamOpenId(query: Record<string, string>): Promise<string | null> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
for (const [key, value] of Object.entries(query)) {
|
||||||
|
params.set(key, String(value));
|
||||||
|
}
|
||||||
|
params.set('openid.mode', 'check_authentication');
|
||||||
|
|
||||||
|
const resp = await fetch(`https://steamcommunity.com/openid/login?${params.toString()}`);
|
||||||
|
const text = await resp.text();
|
||||||
|
if (!text.includes('is_valid:true')) return null;
|
||||||
|
|
||||||
|
const claimedId = String(query['openid.claimed_id'] || '');
|
||||||
|
const match = claimedId.match(/\/id\/(\d+)$/);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSteamProfile(steamId: string): Promise<{ personaName: string; avatarUrl: string }> {
|
||||||
|
const url = `https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key=${STEAM_API_KEY}&steamids=${steamId}`;
|
||||||
|
const resp = await fetch(url);
|
||||||
|
if (!resp.ok) throw new Error(`Steam API error: ${resp.status}`);
|
||||||
|
const json = await resp.json() as any;
|
||||||
|
const player = json?.response?.players?.[0];
|
||||||
|
return {
|
||||||
|
personaName: player?.personaname || steamId,
|
||||||
|
avatarUrl: player?.avatarfull || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ── Register Routes ──
|
// ── Register Routes ──
|
||||||
export function registerAuthRoutes(app: express.Application, adminPwd: string): void {
|
export function registerAuthRoutes(app: express.Application, adminPwd: string): void {
|
||||||
|
|
||||||
|
|
@ -142,7 +201,7 @@ export function registerAuthRoutes(app: express.Application, adminPwd: string):
|
||||||
app.get('/api/auth/providers', (_req, res) => {
|
app.get('/api/auth/providers', (_req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
discord: isDiscordConfigured(),
|
discord: isDiscordConfigured(),
|
||||||
steam: false, // Steam OpenID — planned
|
steam: isSteamConfigured(),
|
||||||
admin: !!adminPwd,
|
admin: !!adminPwd,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -158,6 +217,7 @@ export function registerAuthRoutes(app: express.Application, adminPwd: string):
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
provider: session.provider,
|
provider: session.provider,
|
||||||
discordId: session.discordId ?? null,
|
discordId: session.discordId ?? null,
|
||||||
|
steamId: session.steamId ?? null,
|
||||||
username: session.username ?? null,
|
username: session.username ?? null,
|
||||||
avatar: session.avatar ?? null,
|
avatar: session.avatar ?? null,
|
||||||
globalName: session.globalName ?? null,
|
globalName: session.globalName ?? null,
|
||||||
|
|
@ -211,6 +271,58 @@ export function registerAuthRoutes(app: express.Application, adminPwd: string):
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Steam OpenID 2.0 — start
|
||||||
|
app.get('/api/auth/steam', (req, res) => {
|
||||||
|
if (!isSteamConfigured()) {
|
||||||
|
res.status(503).json({ error: 'Steam nicht konfiguriert (STEAM_API_KEY fehlt)' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const realm = getSteamRealm(req);
|
||||||
|
const returnTo = getSteamReturnUrl(req);
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
'openid.ns': 'http://specs.openid.net/auth/2.0',
|
||||||
|
'openid.mode': 'checkid_setup',
|
||||||
|
'openid.return_to': returnTo,
|
||||||
|
'openid.realm': realm,
|
||||||
|
'openid.identity': 'http://specs.openid.net/auth/2.0/identifier_select',
|
||||||
|
'openid.claimed_id': 'http://specs.openid.net/auth/2.0/identifier_select',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[Auth] Steam OpenID redirect → ${returnTo}`);
|
||||||
|
res.redirect(`https://steamcommunity.com/openid/login?${params.toString()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Steam OpenID 2.0 — callback
|
||||||
|
app.get('/api/auth/steam/callback', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const steamId = await verifySteamOpenId(req.query as Record<string, string>);
|
||||||
|
if (!steamId) {
|
||||||
|
res.status(403).send('Steam-Verifizierung fehlgeschlagen.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = await fetchSteamProfile(steamId);
|
||||||
|
|
||||||
|
const session: UserSession = {
|
||||||
|
provider: 'steam',
|
||||||
|
steamId,
|
||||||
|
username: profile.personaName,
|
||||||
|
avatar: profile.avatarUrl || null,
|
||||||
|
iat: Date.now(),
|
||||||
|
exp: Date.now() + SESSION_MAX_AGE * 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const token = signSession(session);
|
||||||
|
res.setHeader('Set-Cookie', `hub_session=${encodeURIComponent(token)}; HttpOnly; Path=/; Max-Age=${SESSION_MAX_AGE}; SameSite=Lax`);
|
||||||
|
console.log(`[Auth] Steam login: ${profile.personaName} (${steamId})`);
|
||||||
|
res.redirect('/');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Auth] Steam callback error:', e);
|
||||||
|
res.status(500).send('Steam Login fehlgeschlagen. Bitte erneut versuchen.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Admin login (via unified modal)
|
// Admin login (via unified modal)
|
||||||
app.post('/api/auth/admin', (req, res) => {
|
app.post('/api/auth/admin', (req, res) => {
|
||||||
if (!adminPwd) { res.status(503).json({ error: 'Admin nicht konfiguriert' }); return; }
|
if (!adminPwd) { res.status(503).json({ error: 'Admin nicht konfiguriert' }); return; }
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import nacl from 'tweetnacl';
|
||||||
import { ChannelType, Events, type VoiceBasedChannel, type VoiceState, type Message } from 'discord.js';
|
import { ChannelType, Events, type VoiceBasedChannel, type VoiceState, type Message } from 'discord.js';
|
||||||
import type { Plugin, PluginContext } from '../../core/plugin.js';
|
import type { Plugin, PluginContext } from '../../core/plugin.js';
|
||||||
import { sseBroadcast } from '../../core/sse.js';
|
import { sseBroadcast } from '../../core/sse.js';
|
||||||
import { getSession } from '../../core/discord-auth.js';
|
import { getSession, getUserId } from '../../core/discord-auth.js';
|
||||||
|
|
||||||
// ── Config (env) ──
|
// ── Config (env) ──
|
||||||
const SOUNDS_DIR = process.env.SOUNDS_DIR ?? '/data/sounds';
|
const SOUNDS_DIR = process.env.SOUNDS_DIR ?? '/data/sounds';
|
||||||
|
|
@ -1245,15 +1245,15 @@ const soundboardPlugin: Plugin = {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── User Sound Preferences (Discord-authenticated) ──
|
// ── User Sound Preferences (Discord / Steam authenticated) ──
|
||||||
// Get current user's entrance/exit sounds
|
// Get current user's entrance/exit sounds
|
||||||
app.get('/api/soundboard/user/sounds', (req, res) => {
|
app.get('/api/soundboard/user/sounds', (req, res) => {
|
||||||
const session = getSession(req);
|
const session = getSession(req);
|
||||||
if (!session?.discordId) {
|
const userId = session ? getUserId(session) : null;
|
||||||
|
if (!userId) {
|
||||||
res.status(401).json({ error: 'Nicht eingeloggt' });
|
res.status(401).json({ error: 'Nicht eingeloggt' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const userId = session.discordId;
|
|
||||||
const entrance = persistedState.entranceSounds?.[userId] ?? null;
|
const entrance = persistedState.entranceSounds?.[userId] ?? null;
|
||||||
const exit = persistedState.exitSounds?.[userId] ?? null;
|
const exit = persistedState.exitSounds?.[userId] ?? null;
|
||||||
res.json({ entrance, exit });
|
res.json({ entrance, exit });
|
||||||
|
|
@ -1262,7 +1262,8 @@ const soundboardPlugin: Plugin = {
|
||||||
// Set entrance sound
|
// Set entrance sound
|
||||||
app.post('/api/soundboard/user/entrance', (req, res) => {
|
app.post('/api/soundboard/user/entrance', (req, res) => {
|
||||||
const session = getSession(req);
|
const session = getSession(req);
|
||||||
if (!session?.discordId) { res.status(401).json({ error: 'Nicht eingeloggt' }); return; }
|
const userId = session ? getUserId(session) : null;
|
||||||
|
if (!userId) { res.status(401).json({ error: 'Nicht eingeloggt' }); return; }
|
||||||
const { fileName } = req.body ?? {};
|
const { fileName } = req.body ?? {};
|
||||||
if (!fileName || typeof fileName !== 'string') { res.status(400).json({ error: 'fileName erforderlich' }); return; }
|
if (!fileName || typeof fileName !== 'string') { res.status(400).json({ error: 'fileName erforderlich' }); return; }
|
||||||
if (!/\.(mp3|wav)$/i.test(fileName)) { res.status(400).json({ error: 'Nur .mp3 oder .wav' }); return; }
|
if (!/\.(mp3|wav)$/i.test(fileName)) { res.status(400).json({ error: 'Nur .mp3 oder .wav' }); return; }
|
||||||
|
|
@ -1279,16 +1280,17 @@ const soundboardPlugin: Plugin = {
|
||||||
})();
|
})();
|
||||||
if (!resolve) { res.status(404).json({ error: 'Datei nicht gefunden' }); return; }
|
if (!resolve) { res.status(404).json({ error: 'Datei nicht gefunden' }); return; }
|
||||||
persistedState.entranceSounds = persistedState.entranceSounds ?? {};
|
persistedState.entranceSounds = persistedState.entranceSounds ?? {};
|
||||||
persistedState.entranceSounds[session.discordId] = resolve;
|
persistedState.entranceSounds[userId] = resolve;
|
||||||
writeState();
|
writeState();
|
||||||
console.log(`[Soundboard] User ${session.username} (${session.discordId}) set entrance: ${resolve}`);
|
console.log(`[Soundboard] User ${session!.username} (${userId}) set entrance: ${resolve}`);
|
||||||
res.json({ ok: true, entrance: resolve });
|
res.json({ ok: true, entrance: resolve });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set exit sound
|
// Set exit sound
|
||||||
app.post('/api/soundboard/user/exit', (req, res) => {
|
app.post('/api/soundboard/user/exit', (req, res) => {
|
||||||
const session = getSession(req);
|
const session = getSession(req);
|
||||||
if (!session?.discordId) { res.status(401).json({ error: 'Nicht eingeloggt' }); return; }
|
const userId = session ? getUserId(session) : null;
|
||||||
|
if (!userId) { res.status(401).json({ error: 'Nicht eingeloggt' }); return; }
|
||||||
const { fileName } = req.body ?? {};
|
const { fileName } = req.body ?? {};
|
||||||
if (!fileName || typeof fileName !== 'string') { res.status(400).json({ error: 'fileName erforderlich' }); return; }
|
if (!fileName || typeof fileName !== 'string') { res.status(400).json({ error: 'fileName erforderlich' }); return; }
|
||||||
if (!/\.(mp3|wav)$/i.test(fileName)) { res.status(400).json({ error: 'Nur .mp3 oder .wav' }); return; }
|
if (!/\.(mp3|wav)$/i.test(fileName)) { res.status(400).json({ error: 'Nur .mp3 oder .wav' }); return; }
|
||||||
|
|
@ -1304,33 +1306,35 @@ const soundboardPlugin: Plugin = {
|
||||||
})();
|
})();
|
||||||
if (!resolve) { res.status(404).json({ error: 'Datei nicht gefunden' }); return; }
|
if (!resolve) { res.status(404).json({ error: 'Datei nicht gefunden' }); return; }
|
||||||
persistedState.exitSounds = persistedState.exitSounds ?? {};
|
persistedState.exitSounds = persistedState.exitSounds ?? {};
|
||||||
persistedState.exitSounds[session.discordId] = resolve;
|
persistedState.exitSounds[userId] = resolve;
|
||||||
writeState();
|
writeState();
|
||||||
console.log(`[Soundboard] User ${session.username} (${session.discordId}) set exit: ${resolve}`);
|
console.log(`[Soundboard] User ${session!.username} (${userId}) set exit: ${resolve}`);
|
||||||
res.json({ ok: true, exit: resolve });
|
res.json({ ok: true, exit: resolve });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove entrance sound
|
// Remove entrance sound
|
||||||
app.delete('/api/soundboard/user/entrance', (req, res) => {
|
app.delete('/api/soundboard/user/entrance', (req, res) => {
|
||||||
const session = getSession(req);
|
const session = getSession(req);
|
||||||
if (!session?.discordId) { res.status(401).json({ error: 'Nicht eingeloggt' }); return; }
|
const userId = session ? getUserId(session) : null;
|
||||||
|
if (!userId) { res.status(401).json({ error: 'Nicht eingeloggt' }); return; }
|
||||||
if (persistedState.entranceSounds) {
|
if (persistedState.entranceSounds) {
|
||||||
delete persistedState.entranceSounds[session.discordId];
|
delete persistedState.entranceSounds[userId];
|
||||||
writeState();
|
writeState();
|
||||||
}
|
}
|
||||||
console.log(`[Soundboard] User ${session.username} (${session.discordId}) removed entrance sound`);
|
console.log(`[Soundboard] User ${session!.username} (${userId}) removed entrance sound`);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove exit sound
|
// Remove exit sound
|
||||||
app.delete('/api/soundboard/user/exit', (req, res) => {
|
app.delete('/api/soundboard/user/exit', (req, res) => {
|
||||||
const session = getSession(req);
|
const session = getSession(req);
|
||||||
if (!session?.discordId) { res.status(401).json({ error: 'Nicht eingeloggt' }); return; }
|
const userId = session ? getUserId(session) : null;
|
||||||
|
if (!userId) { res.status(401).json({ error: 'Nicht eingeloggt' }); return; }
|
||||||
if (persistedState.exitSounds) {
|
if (persistedState.exitSounds) {
|
||||||
delete persistedState.exitSounds[session.discordId];
|
delete persistedState.exitSounds[userId];
|
||||||
writeState();
|
writeState();
|
||||||
}
|
}
|
||||||
console.log(`[Soundboard] User ${session.username} (${session.discordId}) removed exit sound`);
|
console.log(`[Soundboard] User ${session!.username} (${userId}) removed exit sound`);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,9 @@ interface PluginInfo {
|
||||||
|
|
||||||
interface AuthUser {
|
interface AuthUser {
|
||||||
authenticated: boolean;
|
authenticated: boolean;
|
||||||
provider?: 'discord' | 'admin';
|
provider?: 'discord' | 'steam' | 'admin';
|
||||||
discordId?: string;
|
discordId?: string;
|
||||||
|
steamId?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
avatar?: string | null;
|
avatar?: string | null;
|
||||||
globalName?: string | null;
|
globalName?: string | null;
|
||||||
|
|
@ -69,6 +70,8 @@ export default function App() {
|
||||||
// Derived state
|
// Derived state
|
||||||
const isAdmin = user.authenticated && (user.provider === 'admin' || user.isAdmin === true);
|
const isAdmin = user.authenticated && (user.provider === 'admin' || user.isAdmin === true);
|
||||||
const isDiscordUser = user.authenticated && user.provider === 'discord';
|
const isDiscordUser = user.authenticated && user.provider === 'discord';
|
||||||
|
const isSteamUser = user.authenticated && user.provider === 'steam';
|
||||||
|
const isRegularUser = isDiscordUser || isSteamUser;
|
||||||
|
|
||||||
// Electron auto-update state
|
// Electron auto-update state
|
||||||
const isElectron = !!(window as any).electronAPI?.isElectron;
|
const isElectron = !!(window as any).electronAPI?.isElectron;
|
||||||
|
|
@ -239,7 +242,7 @@ export default function App() {
|
||||||
setShowLoginModal(true);
|
setShowLoginModal(true);
|
||||||
} else if (isAdmin) {
|
} else if (isAdmin) {
|
||||||
setShowAdminPanel(true);
|
setShowAdminPanel(true);
|
||||||
} else if (isDiscordUser) {
|
} else if (isRegularUser) {
|
||||||
setShowUserSettings(true);
|
setShowUserSettings(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -294,7 +297,7 @@ export default function App() {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{user.authenticated ? (
|
{user.authenticated ? (
|
||||||
isDiscordUser && user.avatar ? (
|
isRegularUser && user.avatar ? (
|
||||||
<img src={user.avatar} alt="" className="hub-user-avatar" />
|
<img src={user.avatar} alt="" className="hub-user-avatar" />
|
||||||
) : (
|
) : (
|
||||||
<span className="hub-user-icon">{isAdmin ? '\uD83D\uDD27' : '\uD83D\uDC64'}</span>
|
<span className="hub-user-icon">{isAdmin ? '\uD83D\uDD27' : '\uD83D\uDC64'}</span>
|
||||||
|
|
@ -435,11 +438,12 @@ export default function App() {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* User Settings (Discord users) */}
|
{/* User Settings (Discord + Steam users) */}
|
||||||
{showUserSettings && isDiscordUser && user.discordId && (
|
{showUserSettings && isRegularUser && (
|
||||||
<UserSettings
|
<UserSettings
|
||||||
user={{
|
user={{
|
||||||
discordId: user.discordId,
|
id: user.discordId || user.steamId || '',
|
||||||
|
provider: user.provider as 'discord' | 'steam',
|
||||||
username: user.username ?? '',
|
username: user.username ?? '',
|
||||||
avatar: user.avatar ?? null,
|
avatar: user.avatar ?? null,
|
||||||
globalName: user.globalName ?? null,
|
globalName: user.globalName ?? null,
|
||||||
|
|
|
||||||
|
|
@ -63,14 +63,15 @@ export default function LoginModal({ onClose, onAdminLogin, providers }: LoginMo
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Steam — placeholder */}
|
{/* Steam */}
|
||||||
<button className="hub-login-provider-btn steam" disabled title="Bald verfügbar">
|
{providers.steam && (
|
||||||
<svg className="hub-login-provider-icon" viewBox="0 0 24 24" fill="currentColor" width="22" height="22">
|
<a href="/api/auth/steam" className="hub-login-provider-btn steam">
|
||||||
<path d="M11.979 0C5.678 0 .511 4.86.022 11.037l6.432 2.658c.545-.371 1.203-.59 1.912-.59.063 0 .125.004.188.006l2.861-4.142V8.91c0-2.495 2.028-4.524 4.524-4.524 2.494 0 4.524 2.031 4.524 4.527s-2.03 4.525-4.524 4.525h-.105l-4.076 2.911c0 .052.004.105.004.159 0 1.875-1.515 3.396-3.39 3.396-1.635 0-3.016-1.173-3.331-2.727L.436 15.27C1.862 20.307 6.486 24 11.979 24c6.627 0 12.001-5.373 12.001-12S18.606 0 11.979 0zM7.54 18.21l-1.473-.61c.262.543.714.999 1.314 1.25 1.297.539 2.793-.076 3.332-1.375.263-.63.264-1.319.005-1.949s-.75-1.121-1.377-1.383c-.624-.26-1.29-.249-1.878-.03l1.523.63c.956.4 1.409 1.5 1.009 2.455-.397.957-1.497 1.41-2.454 1.012H7.54zm11.415-9.303a3.015 3.015 0 0 0-3.016-3.016 3.015 3.015 0 0 0-3.016 3.016 3.015 3.015 0 0 0 3.016 3.016 3.015 3.015 0 0 0 3.016-3.016zm-5.273-.005c0-1.248 1.013-2.26 2.26-2.26 1.246 0 2.26 1.013 2.26 2.26 0 1.247-1.014 2.26-2.26 2.26-1.248 0-2.26-1.013-2.26-2.26z" />
|
<svg className="hub-login-provider-icon" viewBox="0 0 24 24" fill="currentColor" width="22" height="22">
|
||||||
</svg>
|
<path d="M11.979 0C5.678 0 .511 4.86.022 11.037l6.432 2.658c.545-.371 1.203-.59 1.912-.59.063 0 .125.004.188.006l2.861-4.142V8.91c0-2.495 2.028-4.524 4.524-4.524 2.494 0 4.524 2.031 4.524 4.527s-2.03 4.525-4.524 4.525h-.105l-4.076 2.911c0 .052.004.105.004.159 0 1.875-1.515 3.396-3.39 3.396-1.635 0-3.016-1.173-3.331-2.727L.436 15.27C1.862 20.307 6.486 24 11.979 24c6.627 0 12.001-5.373 12.001-12S18.606 0 11.979 0zM7.54 18.21l-1.473-.61c.262.543.714.999 1.314 1.25 1.297.539 2.793-.076 3.332-1.375.263-.63.264-1.319.005-1.949s-.75-1.121-1.377-1.383c-.624-.26-1.29-.249-1.878-.03l1.523.63c.956.4 1.409 1.5 1.009 2.455-.397.957-1.497 1.41-2.454 1.012H7.54zm11.415-9.303a3.015 3.015 0 0 0-3.016-3.016 3.015 3.015 0 0 0-3.016 3.016 3.015 3.015 0 0 0 3.016 3.016 3.015 3.015 0 0 0 3.016-3.016zm-5.273-.005c0-1.248 1.013-2.26 2.26-2.26 1.246 0 2.26 1.013 2.26 2.26 0 1.247-1.014 2.26-2.26 2.26-1.248 0-2.26-1.013-2.26-2.26z" />
|
||||||
<span>Steam Login</span>
|
</svg>
|
||||||
<span className="hub-login-soon">bald</span>
|
<span>Mit Steam anmelden</span>
|
||||||
</button>
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Admin */}
|
{/* Admin */}
|
||||||
{providers.admin && (
|
{providers.admin && (
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
interface UserInfo {
|
interface UserInfo {
|
||||||
discordId: string;
|
id: string;
|
||||||
|
provider: 'discord' | 'steam';
|
||||||
username: string;
|
username: string;
|
||||||
avatar: string | null;
|
avatar: string | null;
|
||||||
globalName: string | null;
|
globalName: string | null;
|
||||||
|
|
@ -133,7 +134,9 @@ export default function UserSettings({ user, onClose, onLogout }: UserSettingsPr
|
||||||
)}
|
)}
|
||||||
<div className="hub-usettings-user-info">
|
<div className="hub-usettings-user-info">
|
||||||
<span className="hub-usettings-username">{user.globalName || user.username}</span>
|
<span className="hub-usettings-username">{user.globalName || user.username}</span>
|
||||||
<span className="hub-usettings-discriminator">@{user.username}</span>
|
<span className="hub-usettings-discriminator">
|
||||||
|
{user.provider === 'steam' ? 'Steam' : `@${user.username}`}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="hub-usettings-header-actions">
|
<div className="hub-usettings-header-actions">
|
||||||
|
|
|
||||||
|
|
@ -2456,9 +2456,9 @@ html, body {
|
||||||
border-color: #5865F2;
|
border-color: #5865F2;
|
||||||
background: rgba(88, 101, 242, 0.08);
|
background: rgba(88, 101, 242, 0.08);
|
||||||
}
|
}
|
||||||
.hub-login-provider-btn.steam:hover:not(:disabled) {
|
.hub-login-provider-btn.steam:hover {
|
||||||
border-color: #1b2838;
|
border-color: #66c0f4;
|
||||||
background: rgba(27, 40, 56, 0.15);
|
background: rgba(102, 192, 244, 0.08);
|
||||||
}
|
}
|
||||||
.hub-login-provider-btn.admin:hover {
|
.hub-login-provider-btn.admin:hover {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue