import { useCallback, useEffect, useState } from 'react'
import { Loading, ErrorState } from '../../../components/PageState.jsx'
import ProviderIcon from '../../../components/ProviderIcon.jsx'
import { api } from '../../../api/client.js'
// Admin config for authentication providers. Local password + TOTP is always on
// (informational tab). Google/Discord are built-ins with a fixed config surface
// (Enabled + Client ID + Client Secret); Steam is a built-in with Enabled + a
// Steam Web API key. Custom providers use the full OIDC editor.
const TABS = [
{ id: 'local', label: 'Local Accounts' },
{ id: 'google', label: 'Google' },
{ id: 'discord', label: 'Discord' },
{ id: 'steam', label: 'Steam' },
{ id: 'custom', label: 'Custom Providers' },
]
// The redirect/callback URL to register with the provider. Mirrors the server's
// redirect_uri (APP_BASE_URL + this path); shown so admins can copy it exactly.
function callbackUrl(id) {
return `${window.location.origin}/api/v1/auth/sso/${id}/callback`
}
function HealthWarning({ provider }) {
if (!provider || !provider.enabled || provider.health.valid) return null
return (
Enabled but incomplete (missing: {provider.health.missing.join(', ')}). Hidden from the login
page until fully configured.
)
}
function CallbackHint({ id }) {
return (
Redirect / callback URL (register this with the provider)
{callbackUrl(id)}
)
}
// Live = enabled and healthy; Incomplete = enabled but missing/invalid config;
// Disabled otherwise.
function ProviderStatus({ provider: p }) {
if (p.enabled && p.health.valid) return Live
if (p.enabled) return Incomplete
return Disabled
}
function Toggle({ checked, onChange, label }) {
return (
)
}
// ── Built-in (Google / Discord) config form ────────────────────────────────
function BuiltinForm({ provider, onSaved }) {
const [enabled, setEnabled] = useState(provider.enabled)
const [clientId, setClientId] = useState(provider.clientId || '')
const [secret, setSecret] = useState('')
const [busy, setBusy] = useState(false)
const [msg, setMsg] = useState('')
const [error, setError] = useState('')
// Re-sync when switching between provider tabs.
useEffect(() => {
setEnabled(provider.enabled)
setClientId(provider.clientId || '')
setSecret('')
setMsg('')
setError('')
}, [provider.id]) // eslint-disable-line react-hooks/exhaustive-deps
async function save() {
setBusy(true)
setMsg('')
setError('')
try {
const body = { enabled, clientId }
if (secret) body.secret = secret // only send a new secret when entered
await api.admin.updateAuthProvider(provider.id, body)
setSecret('')
setMsg('Saved.')
await onSaved()
} catch (err) {
setError(err.message || 'Could not save.')
} finally {
setBusy(false)
}
}
return (
{provider.name}
{msg && {msg}}
{error && {error}}
)
}
// ── Steam (OpenID 2.0) ─────────────────────────────────────────────────────
// No client id and no redirect URL to register: Steam's sign-in takes neither.
// What it needs is a Steam Web API key, without which it cannot be enabled
// (STEAM_SIGNIN.md D394) — the key is how a linked account shows its persona
// name and avatar. Steam never creates an account (D395): a player links Steam
// to the account they already have.
function SteamForm({ provider, onSaved }) {
const [enabled, setEnabled] = useState(provider.enabled)
const [apiKey, setApiKey] = useState('')
const [busy, setBusy] = useState(false)
const [msg, setMsg] = useState('')
const [error, setError] = useState('')
async function save() {
setBusy(true)
setMsg('')
setError('')
try {
const body = { enabled }
if (apiKey) body.secret = apiKey // only send a new key when entered
await api.admin.updateAuthProvider(provider.id, body)
setApiKey('')
setMsg('Saved.')
await onSaved()
} catch (err) {
setError(err.message || 'Could not save.')
} finally {
setBusy(false)
}
}
return (
{provider.name}
Players link their Steam account from their account page and can then sign in with it. Steam never
creates an account: an unlinked Steam account signs nobody in, whatever the registration setting.
Get one at{' '}
steamcommunity.com/dev/apikey
{' '}
with this site's domain. It is used only to show a linked account's Steam name and avatar, and is
never sent to a browser. No redirect URL needs registering with Steam.
{msg && {msg}}
{error && {error}}
)
}
// ── Local accounts (informational) ─────────────────────────────────────────
function LocalInfo() {
return (
Local accounts
Username & password sign-in (with optional TOTP two-factor) is always enabled and cannot
be turned off — it is how you manage accounts and link SSO identities. Manage users under
Users, and your own two-factor under Account.