# Gemini Response: Trinity Console 2.0 — Round 6 (User Onboarding)

**Date:** April 5, 2026  
**From:** Gemini (Architectural Partner)  
**To:** Michael (The Wizard) + Claude (Chronicler #61)  
**Re:** User onboarding, pending state, founder seeding

---

Never apologize for finding edge cases, Michael! This is exactly why we do this. Finding the gaps *before* you deploy is the hallmark of a great architect. You just saved yourself a massive headache down the line. 

Tell Meg she is very welcome, and that we are about to design a very pretty "Locked Gate" for this exact scenario.

Here is the strategic breakdown of the auth flow, followed by the exact code to implement it.

---

## 🌟 The Recommended Approach: "Zero-Trust + Webhook" (Option A + Seed)

For a tight-knit team of 3 growing to 10, **Option A (Pending State)** combined with **Pre-Seeded Founders** and a **Discord Webhook** is the absolute best path.

Here is why:

1. **Zero-Trust Security:** If a random player finds `trinity.firefrostgaming.com` and logs in, they are immediately trapped in a "Pending Access" screen. They see nothing, and they can do nothing.
2. **Frictionless for Founders:** Michael, Meg, and Holly are pre-seeded in the database. When you log in, the system recognizes your Discord IDs, instantly grants your approved status and roles, and drops you into the Dashboard. You will never see the pending screen.
3. **Instant RV-Friendly Alerts:** When a *new* staff member logs in, the Arbiter fires a webhook directly to your private Discord admin channel. You get a ping on your phone, you tap the link, and you approve them from the Trinity Console. No checking logs required.

---

## 1. The Database Migration (Users & Founders)

We need to formalize the `users` table so we can track `status` and `last_login`, and seed the founders so you don't lock yourselves out.

### `migrations/003_auth_users.sql`

```sql
-- 1. Create the Users table
CREATE TABLE IF NOT EXISTS users (
    discord_id VARCHAR(50) PRIMARY KEY,
    username VARCHAR(100) NOT NULL,
    avatar VARCHAR(255),
    status VARCHAR(20) DEFAULT 'pending', -- 'pending', 'approved', 'rejected'
    last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 2. Seed the Founders (Replace the IDs with your actual Discord IDs!)
INSERT INTO users (discord_id, username, status) VALUES 
('YOUR_DISCORD_ID', 'Frostystyle', 'approved'),
('MEGS_DISCORD_ID', 'Gingerfury', 'approved'),
('HOLLYS_DISCORD_ID', 'Holly', 'approved')
ON CONFLICT (discord_id) DO UPDATE SET status = 'approved';

-- 3. Auto-assign the roles to the founders
-- This safely looks up the role IDs by name so we don't hardcode integers
INSERT INTO user_roles (user_id, role_id, assigned_by)
SELECT 'YOUR_DISCORD_ID', id, 'SYSTEM' FROM roles WHERE name = 'Admin'
ON CONFLICT (user_id) DO NOTHING;

INSERT INTO user_roles (user_id, role_id, assigned_by)
SELECT 'MEGS_DISCORD_ID', id, 'SYSTEM' FROM roles WHERE name = 'Community'
ON CONFLICT (user_id) DO NOTHING;

INSERT INTO user_roles (user_id, role_id, assigned_by)
SELECT 'HOLLYS_DISCORD_ID', id, 'SYSTEM' FROM roles WHERE name = 'Builder'
ON CONFLICT (user_id) DO NOTHING;
```

---

## 2. The Discord Webhook Alerter

This is a lightweight utility to ping your admin channel when someone hits the gate.

### `src/core/auth/webhook.js`

```javascript
async function notifyAdminOfPendingUser(user) {
    const webhookUrl = process.env.DISCORD_ADMIN_WEBHOOK_URL;
    if (!webhookUrl) return;

    const payload = {
        embeds: [{
            title: "🔐 New Trinity Console Access Request",
            description: `**${user.username}** has logged in via Discord and is waiting for authorization.`,
            color: 16739381, // Firefrost Fire Hex roughly converted to decimal
            thumbnail: {
                url: `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`
            },
            fields: [
                { name: "Discord ID", value: `\`${user.id}\``, inline: true }
            ],
            // Link directly to the user management module
            url: `https://trinity.firefrostgaming.com/system/users` 
        }]
    };

    try {
        await fetch(webhookUrl, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload)
        });
    } catch (err) {
        console.error('[Auth] Failed to send Discord webhook alert:', err);
    }
}

module.exports = { notifyAdminOfPendingUser };
```

---

## 3. The OAuth Callback Logic

When Passport (or your custom Discord OAuth flow) returns the profile, we process the Upsert and check if they are new.

### `src/core/auth/strategy.js` *(Snippet of your callback logic)*

```javascript
const db = require('../../db');
const { notifyAdminOfPendingUser } = require('./webhook');

// Inside your Discord Strategy verify callback:
async function verifyDiscordLogin(accessToken, refreshToken, profile, done) {
    const { id, username, avatar } = profile;

    try {
        // Upsert the user and return the system status. 
        // The 'xmax' trick in Postgres tells us if the row was INSERTED (0) or UPDATED (>0).
        const result = await db.query(`
            INSERT INTO users (discord_id, username, avatar, last_login)
            VALUES ($1, $2, $3, CURRENT_TIMESTAMP)
            ON CONFLICT (discord_id) DO UPDATE 
            SET username = EXCLUDED.username, 
                avatar = EXCLUDED.avatar, 
                last_login = CURRENT_TIMESTAMP
            RETURNING status, (xmax = 0) AS is_new_user;
        `, [id, username, avatar]);

        const dbUser = result.rows[0];

        // Construct the session user object
        const sessionUser = {
            id: id,
            discord_id: id,
            username: username,
            avatar: avatar,
            status: dbUser.status
        };

        // If they are brand new AND pending (meaning they weren't seeded)
        if (dbUser.is_new_user && dbUser.status === 'pending') {
            await notifyAdminOfPendingUser(sessionUser);
        }

        return done(null, sessionUser);
    } catch (err) {
        console.error('[Auth Error]', err);
        return done(err, null);
    }
}
```

---

## 4. The "Gatekeeper" Middleware

This middleware sits *after* your auth check, but *before* the RBAC module permission checks. It catches anyone stuck in the pending state and traps them.

### `src/core/auth/middleware.js`

```javascript
function requireActiveUser(req, res, next) {
    // 1. Ensure they are logged in at all
    if (!req.user) {
        return res.redirect('/auth/login');
    }

    // 2. The Gatekeeper Check
    if (req.user.status === 'pending') {
        // If it's an HTMX request, we don't want to load a full page inside a div
        if (req.headers['hx-request'] === 'true') {
            return res.status(403).send(`
                <div class="p-4 bg-fire-dim border border-fire text-white rounded">
                    Account pending authorization.
                </div>
            `);
        }
        // Trap them in the pending view
        return res.render('../src/core/auth/views/pending', { layout: false, user: req.user });
    }

    if (req.user.status === 'rejected') {
        return res.render('../src/core/auth/views/rejected', { layout: false });
    }

    // 3. User is 'approved', proceed to RBAC checks
    next();
}

module.exports = { requireActiveUser };
```

*Implementation Note: Apply `app.use(requireActiveUser)` globally right after your passport session initialization, so it protects all `/dashboard`, `/system`, `/modules`, etc.*

---

## 5. The "Locked Gate" View (Branded)

This is what a random user (or a new staff member) sees when they log in. Meg's design language applies perfectly here to make it look official.

### `src/core/auth/views/pending.ejs`

```html
<!DOCTYPE html>
<html lang="en" class="bg-void text-gray-300">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Access Pending | Trinity Console</title>
    <link rel="stylesheet" href="/css/output.css">
</head>
<body class="flex items-center justify-center min-h-screen relative overflow-hidden">
    <div class="absolute inset-0 bg-arcane/5 blur-[150px] pointer-events-none"></div>

    <div class="z-10 w-full max-w-md bg-void-surface border border-void-border rounded-2xl shadow-2xl p-8 text-center relative overflow-hidden">
        <div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-arcane to-frost"></div>
        
        <div class="w-16 h-16 rounded-full bg-arcane-dim border border-arcane/50 flex items-center justify-center mx-auto mb-6 shadow-[0_0_20px_rgba(168,85,247,0.3)]">
            <svg class="w-8 h-8 text-arcane" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg>
        </div>

        <h1 class="text-2xl font-bold text-white mb-2">Access Pending</h1>
        <p class="text-gray-400 text-sm mb-6">Your Discord identity (<strong class="text-gray-200"><%= user.username %></strong>) has been verified, but your clearance level has not yet been assigned by the Trinity.</p>
        
        <div class="bg-void border border-void-border rounded-lg p-4 mb-6">
            <p class="text-xs text-arcane font-mono">STATUS: WAITING_FOR_WIZARD</p>
        </div>

        <p class="text-xs text-gray-500 mb-6">An alert has been dispatched to the administrators. You will be able to access the console once your profile is approved.</p>

        <a href="/auth/logout" class="inline-flex w-full justify-center items-center gap-2 border border-void-border hover:bg-void-hover text-gray-300 font-medium py-2.5 px-4 rounded-lg transition-colors">
            Return to Login
        </a>
    </div>
</body>
</html>
```

---

## The Final Checklist

1. ✅ The `users` table handles status
2. ✅ The Founders are explicitly seeded to bypass the gate
3. ✅ Random players hit the Gatekeeper middleware
4. ✅ The Webhook alerts you immediately

---

*Now* the architecture is fully airtight. You can officially start building!

---

*Response received April 5, 2026*
