# UniFi MongoDB Admin Schema (v10.0.162)

## Database: `ace`

### `admin` Collection — Admin Accounts

| Field | Type | Required | Description |
|---|---|---|---|
| `_id` | ObjectId | auto | MongoDB ID |
| `name` | string | yes | Display name |
| `email` | string | yes | Login email (username) |
| `x_shadow` | string | yes | SHA-512 crypt password hash (`$6$salt$hash`) |
| `time_created` | int32 (epoch) | yes | Account creation timestamp |
| `last_site_name` | string | no | Last site accessed (set on login) |
| `super_site_permissions` | array | **yes** | Must be `["super"]` for super admin |
| `super_site_role` | string | **yes** | Must be `"super"` |
| `is_super` | bool | **yes** | Must be `true` |
| `last_login_timestamp` | int64 (epoch ms) | auto | Set on login |
| `last_login_ip` | string | auto | Set on login |
| `ui_settings` | object | auto | `{"preferredLanguage":"en"}` etc. |
| `ui_version` | string | auto | UI version string |
| `email_alert_enabled` | bool | no | Email notifications |
| `is_owner` | bool | auto | Set to `true` on first admin |

**Critical:** Without `super_site_permissions: ["super"]`, `super_site_role: "super"`, and `is_super: true`, the admin will log in but see an empty dashboard even if the database has devices and configs.

### `privilege` Collection — Site Access Control

| Field | Type | Required | Description |
|---|---|---|---|
| `_id` | ObjectId | auto | MongoDB ID |
| `admin_id` | ObjectId | **yes** | References `admin._id` — MUST be ObjectId, NOT string |
| `site_id` | ObjectId | **yes** | References `site._id` — MUST be ObjectId, NOT string |
| `role` | string | **yes** | `"SUPER_ADMIN"` or `"ADMIN"` |
| `permissions` | array | yes | `["ALL"]` for full access |
| `is_super` | bool | **yes** | `true` for super admin |

**Critical pitfall:** Using `.str` on ObjectIds when inserting privileges (e.g., `admin._id.str` instead of `admin._id`) creates string references that don't match. The admin logs in but sees NO data — exactly the "no content on screen" symptom.

### `site` Collection

| Field | Type | Description |
|---|---|---|
| `_id` | ObjectId | Site ID |
| `name` | string | `"super"` or `"default"` |
| `desc` | string | Description |
| `attr_hidden_id` | string | Internal site key |
| `attr_hidden` | bool | `true` for "super" site |
| `attr_no_delete` | bool | `true` for both default sites |

A fresh `jacobalberty/unifi` deploy creates two sites automatically: `"super"` (hidden) and `"default"`.

### Full Admin Creation Script

```javascript
db = db.getSiblingDB("ace");

// 1. Generate hash externally: openssl passwd -6 'password'
var hash = "$6$salt$hash";
var now = Math.floor(new Date().getTime() / 1000);

// 2. Create admin
var result = db.admin.insertOne({
    name: "Admin Name",
    email: "admin@example.com",
    x_shadow: hash,
    time_created: now,
    last_site_name: "default",
    super_site_permissions: ["super"],
    super_site_role: "super",
    is_super: true
});

// 3. Create privileges for ALL sites
var admin = db.admin.findOne({email: "admin@example.com"});
var sites = db.site.find({}).toArray();
sites.forEach(function(site) {
    db.privilege.insertOne({
        admin_id: admin._id,       // ObjectId, NOT .str
        site_id: site._id,         // ObjectId, NOT .str
        role: "SUPER_ADMIN",
        permissions: ["ALL"],
        is_super: true
    });
});

print("Admin created: " + admin._id);
print("Privileges: " + db.privilege.count());
```

### Verifying Permissions After Login

```bash
# Login
curl -sk -c /tmp/uf-cookie -X POST https://localhost:8443/api/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin@example.com","password":"password"}'

# Check self — should show is_super:true
curl -sk -b /tmp/uf-cookie https://localhost:8443/api/self | python3 -m json.tool
```

If `is_super` is `false` or `super_site_permissions` is `[]`, the MongoDB update didn't take — likely the admin document was overwritten by the login process or the update used the wrong query.
