- SQLite database with full schema: users, roles, permissions, role_permissions, user_roles, services, audit_log - RBAC engine with wildcard permission resolution (*.*.*) - Automatic v2→v3 migration from JSON files - 5 default roles: super_admin, admin, editor, user, viewer - Feature registration for APP, GGL, FDX (119 permissions) - 8 services seeded - Full API: roles CRUD, permission check, user-role assignment, feature registration, audit log, stats - Backward compatible with existing auth flows
634 lines
31 KiB
HTML
634 lines
31 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>User Management — Documentation</title>
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body { font-family: 'Inter', sans-serif; background: #0a0a0f; color: #e2e8f0; min-height: 100vh; }
|
|
|
|
.container { max-width: 860px; margin: 0 auto; padding: 24px; }
|
|
|
|
.back-link { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: #64748b; text-decoration: none; margin-bottom: 20px; transition: color 0.15s; }
|
|
.back-link:hover { color: #94a3b8; }
|
|
|
|
/* Hero */
|
|
.hero {
|
|
background: linear-gradient(135deg, rgba(30,30,45,0.9), rgba(20,20,35,0.95));
|
|
border: 1px solid rgba(59,130,246,0.3); border-radius: 16px;
|
|
padding: 32px; margin-bottom: 20px;
|
|
}
|
|
.hero h1 { font-size: 28px; font-weight: 800; letter-spacing: -0.03em; }
|
|
.hero h1 span { color: #3b82f6; }
|
|
.hero-sub { font-size: 14px; color: #94a3b8; margin-top: 6px; }
|
|
.badge-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 16px; }
|
|
.badge { display: inline-block; padding: 4px 12px; border-radius: 6px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
.badge-blue { background: rgba(59,130,246,0.15); color: #60a5fa; border: 1px solid rgba(59,130,246,0.25); }
|
|
.badge-green { background: rgba(34,197,94,0.15); color: #4ade80; border: 1px solid rgba(34,197,94,0.25); }
|
|
.badge-purple { background: rgba(168,85,247,0.15); color: #c084fc; border: 1px solid rgba(168,85,247,0.25); }
|
|
.badge-amber { background: rgba(234,179,8,0.15); color: #fbbf24; border: 1px solid rgba(234,179,8,0.25); }
|
|
|
|
/* Sections */
|
|
.doc-section {
|
|
background: linear-gradient(135deg, rgba(30,30,45,0.9), rgba(20,20,35,0.95));
|
|
border: 1px solid rgba(100,100,140,0.2); border-radius: 16px;
|
|
padding: 24px 28px; margin-bottom: 16px;
|
|
transition: border-color 0.3s;
|
|
}
|
|
.doc-section:hover { border-color: rgba(140,140,200,0.35); }
|
|
.doc-section h2 { font-size: 20px; font-weight: 700; margin-bottom: 12px; display: flex; align-items: center; gap: 10px; }
|
|
.doc-section h3 { font-size: 15px; font-weight: 700; margin: 16px 0 8px; color: #c8d4ff; }
|
|
.doc-section h4 { font-size: 13px; font-weight: 700; margin: 12px 0 6px; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
.doc-section p { font-size: 14px; line-height: 1.7; color: #94a3b8; margin-bottom: 10px; }
|
|
.doc-section ul { padding-left: 20px; margin-bottom: 10px; }
|
|
.doc-section li { font-size: 14px; line-height: 1.7; color: #94a3b8; margin-bottom: 4px; }
|
|
.doc-section strong { color: #e2e8f0; }
|
|
.doc-section code { background: rgba(59,130,246,0.1); color: #93c5fd; padding: 2px 6px; border-radius: 4px; font-size: 12px; font-family: 'SF Mono', 'Fira Code', monospace; }
|
|
.doc-section a { color: #60a5fa; text-decoration: none; }
|
|
.doc-section a:hover { text-decoration: underline; }
|
|
|
|
.section-num {
|
|
width: 32px; height: 32px; border-radius: 50%;
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
font-weight: 800; font-size: 14px; flex-shrink: 0;
|
|
background: rgba(59,130,246,0.15); color: #60a5fa;
|
|
}
|
|
|
|
/* Cards */
|
|
.card-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 12px 0; }
|
|
.card {
|
|
background: rgba(15,15,30,0.6); border: 1px solid rgba(100,100,140,0.15);
|
|
border-radius: 10px; padding: 16px;
|
|
}
|
|
.card h4 { margin-top: 0; }
|
|
.card p { font-size: 12px; margin-bottom: 0; }
|
|
|
|
/* Table */
|
|
.data-table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 13px; }
|
|
.data-table th {
|
|
text-align: left; padding: 8px 12px;
|
|
background: rgba(20,20,40,0.8); color: #64748b;
|
|
font-size: 10px; font-weight: 700; text-transform: uppercase;
|
|
letter-spacing: 0.08em; border-bottom: 1px solid rgba(100,100,140,0.15);
|
|
}
|
|
.data-table td { padding: 8px 12px; color: #94a3b8; border-bottom: 1px solid rgba(100,100,140,0.08); }
|
|
.data-table tr:hover td { background: rgba(40,40,65,0.3); }
|
|
|
|
/* Callout */
|
|
.callout {
|
|
background: rgba(250,180,50,0.06); border: 1px solid rgba(250,180,50,0.2);
|
|
border-radius: 10px; padding: 14px 18px; margin: 12px 0;
|
|
}
|
|
.callout p { color: #d4a855; margin: 0; }
|
|
.callout strong { color: #fbbf24; }
|
|
|
|
.callout-blue {
|
|
background: rgba(59,130,246,0.06); border: 1px solid rgba(59,130,246,0.2);
|
|
}
|
|
.callout-blue p { color: #7dabf0; }
|
|
.callout-blue strong { color: #60a5fa; }
|
|
|
|
.callout-green {
|
|
background: rgba(34,197,94,0.06); border: 1px solid rgba(34,197,94,0.2);
|
|
}
|
|
.callout-green p { color: #6dbf8b; }
|
|
.callout-green strong { color: #4ade80; }
|
|
|
|
/* Steps */
|
|
.step-list { counter-reset: step; list-style: none; padding: 0; }
|
|
.step-list li {
|
|
counter-increment: step; padding: 10px 0 10px 48px; position: relative;
|
|
border-bottom: 1px solid rgba(100,100,140,0.08);
|
|
}
|
|
.step-list li:last-child { border-bottom: none; }
|
|
.step-list li::before {
|
|
content: counter(step);
|
|
position: absolute; left: 0; top: 10px;
|
|
width: 32px; height: 32px; border-radius: 50%;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-weight: 800; font-size: 13px;
|
|
background: rgba(59,130,246,0.1); color: #60a5fa;
|
|
border: 1px solid rgba(59,130,246,0.2);
|
|
}
|
|
.step-list li .step-title { font-weight: 700; color: #e2e8f0; font-size: 14px; }
|
|
.step-list li .step-desc { font-size: 13px; color: #64748b; margin-top: 2px; }
|
|
|
|
/* Expandable */
|
|
.expandable { cursor: pointer; }
|
|
.expandable-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 0; }
|
|
.expandable-header::after { content: '▸'; color: #4a4a6a; transition: transform 0.2s; font-size: 12px; }
|
|
.expandable.open .expandable-header::after { transform: rotate(90deg); }
|
|
.expandable-body { display: none; padding: 0 0 10px; }
|
|
.expandable.open .expandable-body { display: block; }
|
|
|
|
/* Code block */
|
|
.code-block {
|
|
background: rgba(10,10,20,0.8); border: 1px solid rgba(100,100,140,0.15);
|
|
border-radius: 8px; padding: 14px 18px; margin: 10px 0;
|
|
font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px;
|
|
color: #94a3b8; line-height: 1.6; overflow-x: auto; white-space: pre;
|
|
}
|
|
.code-block .method { color: #60a5fa; font-weight: 700; }
|
|
.code-block .url { color: #4ade80; }
|
|
.code-block .comment { color: #475569; }
|
|
.code-block .key { color: #c084fc; }
|
|
.code-block .val { color: #fbbf24; }
|
|
|
|
@media (max-width: 640px) {
|
|
.card-grid { grid-template-columns: 1fr; }
|
|
.container { padding: 16px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
|
|
<a href="/" class="back-link">← Back to User Management</a>
|
|
|
|
<!-- Hero -->
|
|
<div class="hero">
|
|
<h1>🔐 <span>User Management</span></h1>
|
|
<p class="hero-sub">User management, scoped access control, and multi-method authentication for scottfelten.com services.</p>
|
|
<div class="badge-row">
|
|
<span class="badge badge-green">v2.0 — Live</span>
|
|
<span class="badge badge-blue">18 Services</span>
|
|
<span class="badge badge-purple">3 Auth Methods</span>
|
|
<span class="badge badge-amber">March 2026</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 1. Overview -->
|
|
<div class="doc-section">
|
|
<h2><span class="section-num">1</span> Overview</h2>
|
|
<p>
|
|
User Management is the <strong>identity and access layer</strong> for all scottfelten.com services.
|
|
It controls who can sign in, what they can access, and how they authenticate — all from a single dashboard.
|
|
</p>
|
|
|
|
<div class="card-grid">
|
|
<div class="card" style="border-left: 3px solid #3b82f6;">
|
|
<h4 style="color:#60a5fa">🔵 Google SSO</h4>
|
|
<p>Sign in with Google. Primary method for team members and close collaborators. Powered by oauth2-proxy.</p>
|
|
</div>
|
|
<div class="card" style="border-left: 3px solid #a855f7;">
|
|
<h4 style="color:#c084fc">✨ Magic Link</h4>
|
|
<p>One-time login link sent to email. No password needed. Great for clients and external partners.</p>
|
|
</div>
|
|
<div class="card" style="border-left: 3px solid #eab308;">
|
|
<h4 style="color:#fbbf24">🔑 Email + Password</h4>
|
|
<p>Traditional login with bcrypt hashing. 12-char minimum. Lockout protection. Password reset via email.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="callout callout-blue">
|
|
<p>💡 <strong>How it works:</strong> Every scottfelten.com service checks with User Management before letting anyone in.
|
|
When a user visits any service, User Management verifies their session and checks if they have permission for that specific service.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 2. Adding a User -->
|
|
<div class="doc-section">
|
|
<h2><span class="section-num">2</span> Adding a User</h2>
|
|
|
|
<ol class="step-list">
|
|
<li>
|
|
<div class="step-title">Click "+ Add User" on the dashboard</div>
|
|
<div class="step-desc">Opens the user creation modal with all configuration options.</div>
|
|
</li>
|
|
<li>
|
|
<div class="step-title">Enter their email and name</div>
|
|
<div class="step-desc">Email is their login identifier. Must be unique across all users.</div>
|
|
</li>
|
|
<li>
|
|
<div class="step-title">Choose their role</div>
|
|
<div class="step-desc">
|
|
<strong>Client</strong> — external user, limited access.<br>
|
|
<strong>Partner</strong> — business collaborator (e.g., Colum for IAG).<br>
|
|
<strong>Admin</strong> — full access to everything including this dashboard.
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<div class="step-title">Select auth method(s)</div>
|
|
<div class="step-desc">
|
|
Pick one or more: <strong>Google</strong> (they need a Google account), <strong>Magic Link</strong> (any email works),
|
|
or <strong>Password</strong> (you set their initial password, or they can set it later via reset).
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<div class="step-title">Assign services</div>
|
|
<div class="step-desc">
|
|
Check individual services they should access, or select <strong>All Services (*)</strong> for unrestricted access.
|
|
Services are auto-populated from the project inventory — always current.
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<div class="step-title">Optional: Add tags</div>
|
|
<div class="step-desc">Comma-separated labels for organization (e.g., "partner, bpo-expert"). Purely informational for now.</div>
|
|
</li>
|
|
</ol>
|
|
|
|
<div class="callout">
|
|
<p>⚡ <strong>Quick example — Adding Colum (IAG partner):</strong><br>
|
|
Email: colum@whatever.com · Role: Partner · Auth: Magic Link · Services: iag-website, iag-data-viewer, intellicert, usecasegen</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 3. Auth Methods Deep Dive -->
|
|
<div class="doc-section">
|
|
<h2><span class="section-num">3</span> Auth Methods — How Each Works</h2>
|
|
|
|
<h3>🔵 Google SSO</h3>
|
|
<p>The primary auth method. Uses oauth2-proxy to handle the Google OAuth flow. When a user visits a service:</p>
|
|
<ul>
|
|
<li>They're redirected to Google's sign-in page</li>
|
|
<li>After authenticating, oauth2-proxy sets a session cookie</li>
|
|
<li>User Management verifies the cookie and checks their service permissions</li>
|
|
</ul>
|
|
<p><strong>Requirement:</strong> User must have a Google account, and their email must be in the allowlist.</p>
|
|
|
|
<h3>✨ Magic Link</h3>
|
|
<p>Email-based, passwordless login. Perfect for clients and partners who don't use Google.</p>
|
|
<ul>
|
|
<li>User enters their email on the <a href="/login.html">login page</a></li>
|
|
<li>They receive a one-time link (expires in <strong>15 minutes</strong>)</li>
|
|
<li>Clicking the link sets a session cookie valid for <strong>7 days</strong></li>
|
|
<li>No password to remember, no account to create</li>
|
|
</ul>
|
|
|
|
<div class="callout callout-green">
|
|
<p>🔒 <strong>Security:</strong> Links are single-use (consumed on first click) and time-limited. The token is a 32-byte random hex string — not guessable.</p>
|
|
</div>
|
|
|
|
<h3>🔑 Email + Password</h3>
|
|
<p>Traditional login for users who prefer (or need) a persistent credential.</p>
|
|
<ul>
|
|
<li>Passwords hashed with <strong>bcrypt</strong> (12 rounds) — industry standard</li>
|
|
<li><strong>12-character minimum</strong> — enforced on creation and reset</li>
|
|
<li><strong>Lockout:</strong> 5 failed attempts → account locked for 30 minutes (auto-unlocks)</li>
|
|
<li><strong>Password reset:</strong> sends a time-limited reset link (1 hour expiry)</li>
|
|
</ul>
|
|
|
|
<h4>Setting a Password</h4>
|
|
<p>Two ways to set a password for a user:</p>
|
|
<ul>
|
|
<li><strong>At creation:</strong> Enter a password in the "Initial Password" field when adding the user</li>
|
|
<li><strong>After creation:</strong> Click the 🔑 button next to their name in the dashboard</li>
|
|
</ul>
|
|
<p>Users can also reset their own password via the "Forgot password?" link on the login page.</p>
|
|
</div>
|
|
|
|
<!-- 4. Services & Permissions -->
|
|
<div class="doc-section">
|
|
<h2><span class="section-num">4</span> Services & Permissions</h2>
|
|
<p>
|
|
Services are <strong>auto-discovered</strong> from the project inventory every 5 minutes.
|
|
When a new service is deployed, it appears in the services list automatically — no manual updates needed.
|
|
</p>
|
|
|
|
<h3>How Scoping Works</h3>
|
|
<ul>
|
|
<li>Each user has a <code>services[]</code> array listing which services they can access</li>
|
|
<li>When they visit a service, User Management maps the hostname to a service ID and checks if it's in their list</li>
|
|
<li><strong>Wildcard:</strong> <code>*</code> grants access to everything (admin default)</li>
|
|
</ul>
|
|
|
|
<h3>Current Service Map</h3>
|
|
<p>These services are protected by User Management. The list updates automatically from inventory.</p>
|
|
|
|
<table class="data-table">
|
|
<tr><th>Service</th><th>URL</th><th>Category</th></tr>
|
|
<tr><td>inventory</td><td>inv.scottfelten.com</td><td>Core</td></tr>
|
|
<tr><td>access-manager</td><td>users.scottfelten.com</td><td>Core</td></tr>
|
|
<tr><td>taos</td><td>goalstack.scottfelten.com</td><td>Core</td></tr>
|
|
<tr><td>signal-tower</td><td>signal.scottfelten.com</td><td>Intelligence</td></tr>
|
|
<tr><td>customer-360</td><td>c360.scottfelten.com</td><td>Sales</td></tr>
|
|
<tr><td>models-embeddings</td><td>models.scottfelten.com</td><td>AI/ML</td></tr>
|
|
<tr><td>iag-website</td><td>intelligenceadvisorygroup.com</td><td>IAG</td></tr>
|
|
<tr><td>iag-data-viewer</td><td>iag.scottfelten.com</td><td>IAG</td></tr>
|
|
<tr><td>intellicert</td><td>intellicert.app</td><td>IAG</td></tr>
|
|
<tr><td>usecasegen</td><td>app.usecasegen.app</td><td>IAG</td></tr>
|
|
<tr><td colspan="3" style="color:#475569; font-style:italic;">+ 8 more (auto-discovered from inventory)</td></tr>
|
|
</table>
|
|
|
|
<h3>Manual Sync</h3>
|
|
<p>Click the <strong>↻ Sync</strong> button in the dashboard header to force a refresh from inventory. Normally happens automatically every 5 minutes.</p>
|
|
</div>
|
|
|
|
<!-- 5. Login Page -->
|
|
<div class="doc-section">
|
|
<h2><span class="section-num">5</span> The Login Page</h2>
|
|
<p>
|
|
Available at <a href="/login.html">/login.html</a> — this is the <strong>public entry point</strong> for non-Google users.
|
|
It's accessible without authentication.
|
|
</p>
|
|
|
|
<h3>Three Tabs</h3>
|
|
<ul>
|
|
<li><strong>Google</strong> — redirects to Google sign-in (for users with Google SSO enabled)</li>
|
|
<li><strong>Magic Link</strong> — enter email, receive login link</li>
|
|
<li><strong>Password</strong> — enter email + password, with "Forgot password?" reset flow</li>
|
|
</ul>
|
|
|
|
<h3>Redirect Flow</h3>
|
|
<p>When a user hits a protected service without a session, they'll be redirected to the login page with a <code>?rd=</code> parameter.
|
|
After successful auth, they're sent back to the service they originally wanted.</p>
|
|
|
|
<div class="callout callout-blue">
|
|
<p>💡 <strong>Currently:</strong> The default redirect for unauthenticated users goes to Google sign-in.
|
|
To send non-Google users to the multi-auth login page instead, share the direct link:
|
|
<code>https://users.scottfelten.com/login.html?rd=https://SERVICE_URL</code></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 6. Security -->
|
|
<div class="doc-section">
|
|
<h2><span class="section-num">6</span> Security</h2>
|
|
|
|
<table class="data-table">
|
|
<tr><th>Feature</th><th>Detail</th></tr>
|
|
<tr><td>Password hashing</td><td>bcrypt, 12 rounds</td></tr>
|
|
<tr><td>Password minimum</td><td>12 characters</td></tr>
|
|
<tr><td>Login lockout</td><td>5 failed attempts → 30-minute lock (auto-unlocks)</td></tr>
|
|
<tr><td>Magic link expiry</td><td>15 minutes, single-use</td></tr>
|
|
<tr><td>Reset token expiry</td><td>1 hour, single-use</td></tr>
|
|
<tr><td>Session duration</td><td>7 days (JWT cookie on .scottfelten.com)</td></tr>
|
|
<tr><td>Cookie flags</td><td>httpOnly, secure, sameSite=lax</td></tr>
|
|
<tr><td>Token entropy</td><td>32 bytes (256-bit) random hex</td></tr>
|
|
<tr><td>Email enumeration</td><td>Prevented — all responses are identical regardless of email existence</td></tr>
|
|
</table>
|
|
|
|
<h3>Session Types</h3>
|
|
<p>Two session mechanisms coexist:</p>
|
|
<ul>
|
|
<li><strong>oauth2-proxy cookie</strong> — for Google SSO users (managed by oauth2-proxy)</li>
|
|
<li><strong>_am_session cookie</strong> — for magic link and password users (JWT, managed by User Management)</li>
|
|
</ul>
|
|
<p>The <code>/auth/check</code> endpoint tries the <code>_am_session</code> first, then falls back to oauth2-proxy. Either way, scoped access is enforced.</p>
|
|
</div>
|
|
|
|
<!-- 7. API Reference -->
|
|
<div class="doc-section">
|
|
<h2><span class="section-num">7</span> API Reference</h2>
|
|
<p>All admin endpoints require authentication (Google SSO or _am_session with admin role).</p>
|
|
|
|
<h3>Authentication</h3>
|
|
|
|
<div class="expandable" onclick="this.classList.toggle('open')">
|
|
<div class="expandable-header"><strong>POST /auth/magic/request</strong> — Request a magic link</div>
|
|
<div class="expandable-body">
|
|
<p>Public endpoint. Sends a login link to the provided email (if registered).</p>
|
|
<div class="code-block"><span class="method">POST</span> <span class="url">/auth/magic/request</span>
|
|
<span class="comment">// Body:</span>
|
|
{ <span class="key">"email"</span>: <span class="val">"user@example.com"</span>, <span class="key">"redirect"</span>: <span class="val">"https://inv.scottfelten.com"</span> }
|
|
|
|
<span class="comment">// Response (always the same, prevents email enumeration):</span>
|
|
{ <span class="key">"ok"</span>: true, <span class="key">"message"</span>: <span class="val">"If that email is registered, a login link has been sent."</span> }</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="expandable" onclick="this.classList.toggle('open')">
|
|
<div class="expandable-header"><strong>POST /auth/password/login</strong> — Email + password login</div>
|
|
<div class="expandable-body">
|
|
<p>Public endpoint. Returns session cookie on success.</p>
|
|
<div class="code-block"><span class="method">POST</span> <span class="url">/auth/password/login</span>
|
|
<span class="comment">// Body:</span>
|
|
{ <span class="key">"email"</span>: <span class="val">"user@example.com"</span>, <span class="key">"password"</span>: <span class="val">"their-password"</span> }
|
|
|
|
<span class="comment">// Success:</span>
|
|
{ <span class="key">"ok"</span>: true, <span class="key">"user"</span>: { <span class="key">"email"</span>: ..., <span class="key">"name"</span>: ..., <span class="key">"role"</span>: ... } }
|
|
|
|
<span class="comment">// Failure (with lockout tracking):</span>
|
|
{ <span class="key">"error"</span>: <span class="val">"Invalid email or password"</span>, <span class="key">"attemptsRemaining"</span>: 3 }
|
|
|
|
<span class="comment">// Locked out:</span>
|
|
{ <span class="key">"error"</span>: <span class="val">"Account locked. Try again in 28 minutes."</span>, <span class="key">"locked"</span>: true }</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="expandable" onclick="this.classList.toggle('open')">
|
|
<div class="expandable-header"><strong>POST /auth/password/set</strong> — Set password for a user (admin)</div>
|
|
<div class="expandable-body">
|
|
<div class="code-block"><span class="method">POST</span> <span class="url">/auth/password/set</span>
|
|
<span class="comment">// Body:</span>
|
|
{ <span class="key">"email"</span>: <span class="val">"user@example.com"</span>, <span class="key">"password"</span>: <span class="val">"min-12-characters"</span> }</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="expandable" onclick="this.classList.toggle('open')">
|
|
<div class="expandable-header"><strong>POST /auth/password/reset-request</strong> — Request password reset</div>
|
|
<div class="expandable-body">
|
|
<p>Public endpoint. Sends a reset link to the email if registered.</p>
|
|
<div class="code-block"><span class="method">POST</span> <span class="url">/auth/password/reset-request</span>
|
|
{ <span class="key">"email"</span>: <span class="val">"user@example.com"</span> }</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="expandable" onclick="this.classList.toggle('open')">
|
|
<div class="expandable-header"><strong>POST /auth/password/reset</strong> — Execute password reset</div>
|
|
<div class="expandable-body">
|
|
<div class="code-block"><span class="method">POST</span> <span class="url">/auth/password/reset</span>
|
|
{ <span class="key">"token"</span>: <span class="val">"hex-token-from-email"</span>, <span class="key">"password"</span>: <span class="val">"new-password-12+"</span> }</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="expandable" onclick="this.classList.toggle('open')">
|
|
<div class="expandable-header"><strong>GET /auth/session</strong> — Check current session</div>
|
|
<div class="expandable-body">
|
|
<p>Public endpoint. Returns session info or <code>{ authenticated: false }</code>.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="expandable" onclick="this.classList.toggle('open')">
|
|
<div class="expandable-header"><strong>POST /auth/logout</strong> — Clear session</div>
|
|
<div class="expandable-body">
|
|
<p>Clears the <code>_am_session</code> cookie. Note: does not clear the oauth2-proxy session (Google SSO).</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>User Management (Admin)</h3>
|
|
|
|
<div class="expandable" onclick="this.classList.toggle('open')">
|
|
<div class="expandable-header"><strong>GET /api/users</strong> — List all users</div>
|
|
<div class="expandable-body">
|
|
<p>Returns array of users. Password hashes are excluded; includes <code>hasPassword: true/false</code>.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="expandable" onclick="this.classList.toggle('open')">
|
|
<div class="expandable-header"><strong>POST /api/users</strong> — Create a user</div>
|
|
<div class="expandable-body">
|
|
<div class="code-block"><span class="method">POST</span> <span class="url">/api/users</span>
|
|
{
|
|
<span class="key">"email"</span>: <span class="val">"colum@example.com"</span>,
|
|
<span class="key">"name"</span>: <span class="val">"Colum"</span>,
|
|
<span class="key">"role"</span>: <span class="val">"partner"</span>,
|
|
<span class="key">"authMethods"</span>: [<span class="val">"magic-link"</span>],
|
|
<span class="key">"services"</span>: [<span class="val">"iag-website"</span>, <span class="val">"iag-data-viewer"</span>, <span class="val">"intellicert"</span>, <span class="val">"usecasegen"</span>],
|
|
<span class="key">"tags"</span>: [<span class="val">"partner"</span>],
|
|
<span class="key">"password"</span>: <span class="val">"optional-initial-password"</span>
|
|
}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="expandable" onclick="this.classList.toggle('open')">
|
|
<div class="expandable-header"><strong>PUT /api/users/:email</strong> — Update a user</div>
|
|
<div class="expandable-body">
|
|
<p>Partial update — only send the fields you want to change.</p>
|
|
<div class="code-block"><span class="method">PUT</span> <span class="url">/api/users/colum@example.com</span>
|
|
{ <span class="key">"services"</span>: [<span class="val">"iag-website"</span>, <span class="val">"intellicert"</span>] }</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="expandable" onclick="this.classList.toggle('open')">
|
|
<div class="expandable-header"><strong>DELETE /api/users/:email</strong> — Remove a user</div>
|
|
<div class="expandable-body">
|
|
<p>Removes the user and updates the oauth2-proxy allowlist. Cannot delete yourself.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Services</h3>
|
|
|
|
<div class="expandable" onclick="this.classList.toggle('open')">
|
|
<div class="expandable-header"><strong>GET /api/services</strong> — List available services</div>
|
|
<div class="expandable-body"><p>Returns sorted array of unique service IDs (deduplicated from hostname mappings).</p></div>
|
|
</div>
|
|
|
|
<div class="expandable" onclick="this.classList.toggle('open')">
|
|
<div class="expandable-header"><strong>GET /api/services/map</strong> — Full hostname → service map</div>
|
|
<div class="expandable-body"><p>Returns the complete mapping of hostnames to service IDs. Useful for debugging.</p></div>
|
|
</div>
|
|
|
|
<div class="expandable" onclick="this.classList.toggle('open')">
|
|
<div class="expandable-header"><strong>POST /api/services/sync</strong> — Force sync from inventory</div>
|
|
<div class="expandable-body"><p>Pulls fresh data from inventory API. Normally happens automatically every 5 minutes.</p></div>
|
|
</div>
|
|
|
|
<h3>Configuration</h3>
|
|
|
|
<div class="expandable" onclick="this.classList.toggle('open')">
|
|
<div class="expandable-header"><strong>PUT /api/config/email</strong> — Configure email transport</div>
|
|
<div class="expandable-body">
|
|
<div class="code-block"><span class="method">PUT</span> <span class="url">/api/config/email</span>
|
|
{
|
|
<span class="key">"host"</span>: <span class="val">"smtp.gmail.com"</span>,
|
|
<span class="key">"port"</span>: 587,
|
|
<span class="key">"user"</span>: <span class="val">"scott@scottfelten.com"</span>,
|
|
<span class="key">"pass"</span>: <span class="val">"google-app-password"</span>,
|
|
<span class="key">"from"</span>: <span class="val">"TARS <scott@scottfelten.com>"</span>
|
|
}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 8. Architecture -->
|
|
<div class="doc-section">
|
|
<h2><span class="section-num">8</span> Architecture</h2>
|
|
|
|
<h3>Auth Flow</h3>
|
|
<div class="code-block"><span class="comment">User visits service.scottfelten.com</span>
|
|
↓
|
|
<span class="comment">nginx auth_request → User Management /auth/check</span>
|
|
↓
|
|
<span class="comment">Check 1: _am_session cookie? (magic link / password users)</span>
|
|
↓ no
|
|
<span class="comment">Check 2: oauth2-proxy session? (Google SSO users)</span>
|
|
↓ no
|
|
<span class="comment">401 → redirect to Google sign-in (or login page for non-Google users)</span>
|
|
↓ yes (either check)
|
|
<span class="comment">Look up user → check service scope → 200 (allow) or 403 (denied)</span></div>
|
|
|
|
<h3>Infrastructure</h3>
|
|
<table class="data-table">
|
|
<tr><th>Component</th><th>Location</th><th>Port</th></tr>
|
|
<tr><td>User Management</td><td>VPS systemd: access-manager.service</td><td>3030</td></tr>
|
|
<tr><td>oauth2-proxy</td><td>VPS systemd: oauth2-proxy.service</td><td>4180</td></tr>
|
|
<tr><td>Inventory API</td><td>VPS systemd: inventory-api.service</td><td>3025</td></tr>
|
|
<tr><td>nginx</td><td>All services proxied, auth_request on each</td><td>443</td></tr>
|
|
</table>
|
|
|
|
<h3>Data Files</h3>
|
|
<table class="data-table">
|
|
<tr><th>File</th><th>Purpose</th></tr>
|
|
<tr><td><code>data/users.json</code></td><td>User store (email, role, services, passwordHash, authMethods)</td></tr>
|
|
<tr><td><code>data/services-cache.json</code></td><td>Cached service map from inventory (refreshed every 5 min)</td></tr>
|
|
<tr><td><code>data/tokens.json</code></td><td>Active magic link and reset tokens (auto-cleaned)</td></tr>
|
|
<tr><td><code>data/lockouts.json</code></td><td>Failed login tracking and lockout state</td></tr>
|
|
<tr><td><code>data/config.json</code></td><td>JWT secret, SMTP config, email settings</td></tr>
|
|
<tr><td><code>/etc/oauth2-proxy-users.txt</code></td><td>oauth2-proxy email allowlist (auto-synced from users.json)</td></tr>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- 9. Email Setup -->
|
|
<div class="doc-section">
|
|
<h2><span class="section-num">9</span> Setting Up Email</h2>
|
|
<p>
|
|
Email is required for magic links and password resets. Without it, these features log to the server console
|
|
(useful for testing, not for real users).
|
|
</p>
|
|
|
|
<h3>Option A: Google Workspace SMTP (Recommended)</h3>
|
|
<ol class="step-list">
|
|
<li>
|
|
<div class="step-title">Create a Google App Password</div>
|
|
<div class="step-desc">Go to <a href="https://myaccount.google.com/apppasswords" target="_blank">myaccount.google.com/apppasswords</a> → generate a password for "Mail" on "Other device"</div>
|
|
</li>
|
|
<li>
|
|
<div class="step-title">Configure via API</div>
|
|
<div class="step-desc">
|
|
<div class="code-block"><span class="method">PUT</span> <span class="url">https://users.scottfelten.com/api/config/email</span>
|
|
{
|
|
<span class="key">"host"</span>: <span class="val">"smtp.gmail.com"</span>,
|
|
<span class="key">"port"</span>: 587,
|
|
<span class="key">"user"</span>: <span class="val">"scott@scottfelten.com"</span>,
|
|
<span class="key">"pass"</span>: <span class="val">"xxxx-xxxx-xxxx-xxxx"</span>,
|
|
<span class="key">"from"</span>: <span class="val">"TARS <scott@scottfelten.com>"</span>
|
|
}</div>
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<div class="step-title">Test it</div>
|
|
<div class="step-desc">Request a magic link for your own email. If it arrives, you're good.</div>
|
|
</li>
|
|
</ol>
|
|
|
|
<h3>Option B: Resend.com</h3>
|
|
<p>Free tier: 100 emails/day. Use <code>smtp.resend.com</code> with your API key as the password.</p>
|
|
</div>
|
|
|
|
<!-- 10. Quick Reference -->
|
|
<div class="doc-section">
|
|
<h2><span class="section-num">10</span> Quick Reference</h2>
|
|
|
|
<table class="data-table">
|
|
<tr><th>Task</th><th>How</th></tr>
|
|
<tr><td>Add a Google SSO user</td><td>Dashboard → + Add User → select Google auth + services</td></tr>
|
|
<tr><td>Add a non-Google user</td><td>Dashboard → + Add User → select Magic Link or Password auth</td></tr>
|
|
<tr><td>Set/change a password</td><td>Dashboard → click 🔑 button next to user</td></tr>
|
|
<tr><td>See who has access to what</td><td>Dashboard — services shown inline per user</td></tr>
|
|
<tr><td>Refresh the services list</td><td>Dashboard → ↻ Sync button</td></tr>
|
|
<tr><td>Send a login link</td><td>Share: <code>users.scottfelten.com/login.html</code></td></tr>
|
|
<tr><td>Check system health</td><td><code>GET /health</code> — shows users, services, email status</td></tr>
|
|
<tr><td>Unlock a locked account</td><td>Wait 30 min (auto-unlocks) or delete <code>data/lockouts.json</code></td></tr>
|
|
<tr><td>Configure email</td><td><code>PUT /api/config/email</code> (see Section 9)</td></tr>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div style="text-align:center; padding: 24px 0; color: #334155; font-size: 11px;">
|
|
User Management v2.0 · Built by TARS · March 2026
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
// Make expandables work
|
|
document.querySelectorAll('.expandable').forEach(el => {
|
|
el.querySelector('.expandable-header').addEventListener('click', () => {
|
|
el.classList.toggle('open');
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|