Document Directus production contract and repair auth

This commit is contained in:
makearmy 2026-07-10 10:51:04 -04:00
parent 0a7ee5ff35
commit 92ce8bce97
32 changed files with 24821 additions and 65 deletions

View file

@ -1,13 +1,12 @@
// app/api/auth/register/route.ts
import { NextResponse } from "next/server";
import { rateLimit, requestIp } from "@/lib/rate-limit";
import {
createDirectusUser,
directusUserExists,
loginDirectus,
} from "@/lib/directus";
const DIRECTUS = (process.env.DIRECTUS_URL || process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, "");
const SERVICE_TOKEN =
process.env.DIRECTUS_SERVICE_TOKEN ||
process.env.DIRECTUS_ADMIN_TOKEN ||
process.env.DIRECTUS_TOKEN_ADMIN_REGISTER || "";
const DEFAULT_ROLE = process.env.DIRECTUS_DEFAULT_ROLE || undefined;
const SECURE = process.env.NODE_ENV === "production";
function bad(message: string, status = 400) {
@ -15,16 +14,18 @@ function bad(message: string, status = 400) {
}
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
async function directusLogin(email: string, password: string) {
const r = await fetch(`${DIRECTUS}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ email, password }),
cache: "no-store",
function isConflict(error: any): boolean {
const code = error?.detail?.errors?.[0]?.extensions?.code;
const message = error?.detail?.errors?.[0]?.message || error?.message || "";
return error?.status === 409 || code === "RECORD_NOT_UNIQUE" || /unique|already exists/i.test(message);
}
function logDirectusFailure(stage: string, error: any) {
console.error(`[auth/register] Directus ${stage} failed`, {
status: error?.status,
code: error?.detail?.errors?.[0]?.extensions?.code,
message: error?.detail?.errors?.[0]?.message || error?.message,
});
const j = await r.json().catch(() => ({}));
if (!r.ok) throw new Error(j?.errors?.[0]?.message || j?.message || `Login failed (${r.status})`);
return j?.data || j;
}
export async function POST(req: Request) {
@ -32,9 +33,6 @@ export async function POST(req: Request) {
if (!rateLimit(`register:${requestIp(req)}`, 5, 60 * 60_000)) {
return bad("Too many registration attempts. Please try again later.", 429);
}
if (!DIRECTUS) return bad("Missing DIRECTUS_URL/NEXT_PUBLIC_API_BASE_URL", 500);
if (!SERVICE_TOKEN) return bad("Missing DIRECTUS_SERVICE_TOKEN / admin token", 500);
const body = await req.json().catch(() => ({} as any));
const email = String(body?.email ?? "").trim().toLowerCase();
const username = String(body?.username ?? "").trim();
@ -46,53 +44,35 @@ export async function POST(req: Request) {
if (password.length < 8) return bad("Password must be at least 8 characters");
if (password !== confirm) return bad("Passwords do not match");
// Optional pre-check to return a friendly 409 instead of a generic Directus error
const existsRes = await fetch(
`${DIRECTUS}/users?filter[_or][0][email][_eq]=${encodeURIComponent(email)}` +
`&filter[_or][1][username][_eq]=${encodeURIComponent(username)}` +
`&fields=id,email,username&limit=1`,
{
headers: {
Authorization: `Bearer ${SERVICE_TOKEN}`,
Accept: "application/json",
},
cache: "no-store",
try {
if (await directusUserExists(email, username)) {
return bad("Email or username already in use", 409);
}
);
const existsJson = await existsRes.json().catch(() => ({}));
if (Array.isArray(existsJson?.data) && existsJson.data.length > 0) {
return bad("Email or username already in use", 409);
} catch (error: any) {
logDirectusFailure("duplicate lookup", error);
return bad("Sign-up is temporarily unavailable.", 503);
}
// Create user with sane defaults
const createPayload: any = {
email,
username,
password,
};
if (DEFAULT_ROLE) createPayload.role = DEFAULT_ROLE;
const createRes = await fetch(`${DIRECTUS}/users`, {
method: "POST",
headers: {
Authorization: `Bearer ${SERVICE_TOKEN}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(createPayload),
cache: "no-store",
});
const cj = await createRes.json().catch(() => ({}));
if (!createRes.ok) {
const msg = cj?.errors?.[0]?.message || cj?.message || `User create failed (${createRes.status})`;
return bad(msg, createRes.status || 500);
let user: { id: string };
try {
// Resolves the production Users role through the Registration Bot
// policy, then creates an active local-auth account in that role.
user = await createDirectusUser({ email, username, password });
} catch (error: any) {
if (isConflict(error)) return bad("Email or username already in use", 409);
logDirectusFailure("user creation", error);
return bad("Sign-up is temporarily unavailable.", 503);
}
// Auto-login (email-based; directus expects "email" even though it's an identifier)
const tokens = await directusLogin(email, password);
let tokens: any;
try {
tokens = await loginDirectus(email, password);
} catch (error: any) {
logDirectusFailure("post-registration login", error);
return bad("Your account was created, but automatic sign-in failed. Please sign in.", 502);
}
const res = NextResponse.json({ ok: true, id: cj?.data?.id || null }, { status: 201 });
const res = NextResponse.json({ ok: true, id: user.id }, { status: 201 });
if (tokens?.access_token) {
res.cookies.set("ma_at", tokens.access_token, {
path: "/",

View file

@ -4,14 +4,28 @@
// ⚠️ NOTE: Do NOT import `headers()` / `cookies()` from "next/headers" here.
// Next 15's types can make those async and break build-time type checks in shared libs.
const BASE = (process.env.DIRECTUS_URL || "").replace(/\/$/, "");
const TOKEN_ADMIN_REGISTER = process.env.DIRECTUS_TOKEN_ADMIN_REGISTER || ""; // server-only
const BASE = (
process.env.DIRECTUS_URL ||
process.env.NEXT_PUBLIC_API_BASE_URL ||
""
).replace(/\/$/, "");
// DIRECTUS_TOKEN_ADMIN_REGISTER is the canonical credential for the production
// Registration Bot policy. Keep the older aliases as fallbacks so an existing
// deployment does not silently lose authentication during migration.
const TOKEN_ADMIN_REGISTER =
process.env.DIRECTUS_TOKEN_ADMIN_REGISTER ||
process.env.DIRECTUS_SERVICE_TOKEN ||
process.env.DIRECTUS_ADMIN_TOKEN ||
""; // server-only
const ROLE_MEMBER_NAME = process.env.DIRECTUS_ROLE_MEMBER_NAME || "Users";
const PROJECTS_COLLECTION = process.env.DIRECTUS_PROJECTS_COLLECTION || "projects";
if (!BASE) console.warn("[directus] Missing DIRECTUS_URL");
if (!BASE) console.warn("[directus] Missing DIRECTUS_URL / NEXT_PUBLIC_API_BASE_URL");
if (!TOKEN_ADMIN_REGISTER)
console.warn("[directus] Missing DIRECTUS_TOKEN_ADMIN_REGISTER (used for registration and username login)");
console.warn(
"[directus] Missing DIRECTUS_TOKEN_ADMIN_REGISTER (or legacy service-token alias) " +
"used for registration and username login"
);
export function bytesFromMB(mb: number) {
return Math.round(mb * 1024 * 1024);
@ -119,7 +133,8 @@ export async function dxDELETE<T = any>(path: string, bearer: string): Promise<T
// ─────────────────────────────────────────────────────────────
export async function directusAdminFetch<T = any>(path: string, init?: RequestInit): Promise<T> {
if (!TOKEN_ADMIN_REGISTER) throw new Error("Missing DIRECTUS_TOKEN_ADMIN_REGISTER");
if (!BASE) throw new Error("Missing Directus base URL");
if (!TOKEN_ADMIN_REGISTER) throw new Error("Missing Directus registration credential");
const res = await fetch(`${BASE}${path}`, {
...init,
headers: {
@ -231,13 +246,25 @@ export async function createDirectusUser(input: {
}
export async function emailForUsername(username: string): Promise<string | null> {
const q = `/users?filter[username][_eq]=${encodeURIComponent(username)}&fields=email&limit=1`;
const clean = username.trim();
if (!clean) return null;
const q = `/users?filter[username][_eq]=${encodeURIComponent(clean)}&fields=email&limit=1`;
const { data } = await directusAdminFetch<{ data: Array<{ email?: string }> }>(q);
const em = data?.[0]?.email;
return em ? String(em) : null;
}
export async function directusUserExists(email: string, username: string): Promise<boolean> {
const q =
`/users?filter[_or][0][email][_eq]=${encodeURIComponent(email)}` +
`&filter[_or][1][username][_eq]=${encodeURIComponent(username)}` +
`&fields=id&limit=1`;
const { data } = await directusAdminFetch<{ data: Array<{ id: string }> }>(q);
return Array.isArray(data) && data.length > 0;
}
export async function loginDirectus(email: string, password: string) {
if (!BASE) throw new Error("Missing Directus base URL");
const res = await fetch(`${BASE}/auth/login`, {
method: "POST",
headers: { Accept: "application/json", "Content-Type": "application/json" },

34
directus/README.md Normal file
View file

@ -0,0 +1,34 @@
# Directus production reference
This directory documents how the Next.js application integrates with the production Directus instance. It is evidence for development and review; it is not an automated migration source and must not be applied to production without a separate, reviewed change process.
## Evidence and authority
- `schema.yaml` is the fresh schema snapshot collected from production Directus 12.1.1 on 2026-07-10. It is the authoritative schema evidence in this directory for that collection time.
- `reference/` contains immutable, sanitized source evidence collected from the same deployment: deployment inventory, Compose and Nginx summaries, environment-variable names, roles, policies, access assignments, permissions, settings metadata, flows, operations, user-field metadata, and system-table layouts.
- `../app/schema.yaml` is an older Directus 11.12.0 snapshot. It remains useful as historical application evidence, but it is not the current production schema. It must remain in place alongside the current snapshot.
- The Markdown files directly under `directus/` are human-readable interpretations of the application source and raw evidence. When prose conflicts with raw evidence, re-check the evidence and update the prose.
- `env.example` is a names-and-placeholders contract. It contains no production values.
The exports intentionally exclude users, user-specific access assignments, credentials, sessions, uploaded data, and flow option values. Consequently, a static credential's actual user/role binding and the detailed behavior inside flows cannot be proven from this material.
## Documents
- `deployment.md`: production topology and explicitly configured versus unobserved settings.
- `auth-contract.md`: application-to-Directus authentication and account operations.
- `permissions.md`: role, policy, field, filter, validation, and preset matrix.
- `flows.md`: flow and operation graphs, with unresolved behavior called out.
- `schema-differences.md`: structural comparison with `../app/schema.yaml`.
- `integration-testing.md`: proposed disposable integration-test environment.
## Refresh procedure
1. Collect from the running production instance without changing it.
2. Sanitize the result before it enters the repository. Exclude secret values, credentials, user rows, email addresses, sessions, cookies, uploaded files, and sensitive flow options.
3. Store a new schema snapshot under `snapshots/`, using a date or version, for example `snapshots/schema-directus-12.1.1-2026-07-10.yaml`. Do not overwrite `schema.yaml` or `../app/schema.yaml` as part of a historical refresh.
4. Store supporting sanitized exports in a correspondingly dated reference location, or clearly record which snapshot they describe.
5. Record the Directus version, database vendor, collection time, and checksums.
6. Compare the new snapshot to the preceding production snapshot and update the human-readable documents only from observed differences.
7. Run a sensitive-data scan and `git diff --check`, then review the complete diff before committing.
The current collection process is described only by its outputs; the exact export commands are not present. That part of the refresh procedure is therefore **unresolved** and should be documented when a repeatable sanitized collection script is added.

37
directus/auth-contract.md Normal file
View file

@ -0,0 +1,37 @@
# Authentication and account contract
Status labels mean:
- **Verified**: directly supported by application source plus the sanitized production exports.
- **Inferred**: the source intends the behavior, but a runtime binding, omitted value, or live test is missing.
- **Unresolved**: the required implementation or evidence is absent.
The app uses a Directus access token as a user bearer, normally stored in the HTTP-only `ma_at` cookie. It stores a refresh token in HTTP-only `ma_rt` and a five-minute recent-authentication marker in HTTP-only `ma_ra`. In production these cookies are `Secure`, `SameSite=Lax`, and path `/` where set by the current auth routes.
## Operation matrix
| Operation | Application route/module | Directus endpoint | Credential and required permission | Success and important failures | Status |
| --- | --- | --- | --- | --- | --- |
| Signup | `POST /api/auth/register`; `app/app/api/auth/register/route.ts` | `GET /users` duplicate lookup; `GET /roles` Users-role lookup; `POST /users`; then `POST /auth/login` | The canonical static server credential is `DIRECTUS_TOKEN_ADMIN_REGISTER`, with legacy service/admin names as fallbacks. It must represent the Registration Bot role/policy or an equivalent identity. Registration policy grants the required role read and user read/create permissions. | Validates all fields, email shape, password length >=8, and confirmation. Resolves the `Users` role, creates an active account with email/username/password, and performs email-based auto-login. Successful login sets `ma_at` and possibly `ma_rt`. Permission/configuration failures are logged server-side and returned as generic temporary-unavailable responses. | Application flow, build, mocked request contract, and policy are **verified**; production static credential-to-role binding still requires deployment verification because users/values were omitted. |
| Duplicate-user check | Same route | `GET /users?filter[...]&fields=id&limit=1` | Registration policy requires `directus_users.read` for `id`, which is exported. | A returned row or create-time uniqueness conflict produces the same generic email-or-username 409. A failed lookup stops signup with a generic 503 instead of continuing with uncertain permissions. | **Verified** source and mocked request behavior; production credential capability requires a live test. |
| Email login | `POST /api/auth/login`; `app/app/api/auth/login/route.ts` | `POST /auth/login` | No bearer is supplied; local email/password credentials are sent to Directus. No collection permission is used for the auth endpoint itself. | On success sets one-hour `ma_at`, optional 30-day `ma_rt`, and optionally five-minute `ma_ra` for a reauth request. Authentication failures are deliberately collapsed to a generic 401. App IP limiter allows 20 attempts/minute per process. | **Verified** implementation; live Directus local-auth behavior is **inferred** pending integration tests. |
| Username login | Same login route; `emailForUsername()` in `app/lib/directus.ts` | Static-token `GET /users` lookup, then `POST /auth/login` with resolved email | The same registration credential used by signup must read `directus_users.username,email`; Registration policy exports those fields. | Missing username gives generic 401. Lookup errors give 503. Resolved email follows email-login behavior. The shared helper accepts `DIRECTUS_URL` with `NEXT_PUBLIC_API_BASE_URL` fallback. | Source, build, required policy, and mocked request contract are **verified**; credential binding and end-to-end production success require a deployment test. |
| Current user | `GET /api/account`, `GET /api/me`, proxy `/api/dx/users/me`, middleware; relevant modules under `app/app/api/` and `app/middleware.ts` | `GET /users/me` with varying field lists | Current user's access token. Users Self Access policy permits own-row read of `id,username,first_name,last_name,email,password,avatar,location,title,description,tags,language,tfa_secret,email_notifications`; Users policy additionally permits public-like `id,username,first_name,avatar,location`. | Returns requested profile fields if field permissions allow. Middleware checks `id` at most once per minute and redirects to reauth on failure. `/api/me` requests `display_name`, which is not present in the exported custom user-field metadata or allowed-field lists, so that field's behavior is unresolved. | Own-row permissions and route calls are **verified**. |
| Token refresh | Token storage occurs in register/login and `app/lib/auth-cookies.ts` | Expected Directus endpoint would be `/auth/refresh`, but no call exists | `ma_rt` is stored, but no application route consumes it. | There is no implemented refresh behavior. Expired/invalid `ma_at` causes middleware to clear `ma_at`/`ma_v` and redirect to sign-in. | **Verified missing implementation**. |
| Logout | `POST` or `GET /api/auth/logout`; `app/app/api/auth/logout/route.ts` | None | No Directus call and no credential required by the handler. | Expires only `ma_at`. It does not revoke the Directus session and does not clear `ma_rt`, `ma_ra`, or `ma_v`. | **Verified**. |
| Password change | `POST`/`PATCH /api/account/password`; `app/app/api/account/password/route.ts` | `GET /users/me`; optionally static-token `GET /users`; `POST /auth/login`; `PATCH /users/me` | Current user token needs own-row read and `directus_users.update` for `password`; Users Self Access grants both. Username verification additionally needs Registration read permission. | Requires current/new values and an eight-character new password. Verifies the current password through local login, then patches only `password`. External-provider rejection depends on reading `provider`, but `provider` is not in the Users Self Access read fields, so this guard may not receive the field. Successful change does not rotate or clear existing app tokens. | Main path and permissions **verified**; provider guard and session effects **unresolved**. |
| Profile changes | `PATCH /api/account` and `PATCH /api/account/profile`; UI primarily uses the latter | `PATCH /users/me` | Current user token. Users Self Access update permits `first_name,last_name,email,avatar,location,password`; it does not permit `username`. | First/last name and location update directly. Email requires `ma_ra=1`; the marker is cleared after a successful sensitive update. UI always includes email in its payload, so its normal save path requests reauthentication even when email is unchanged. Username is classified sensitive but is never added to the patch payload and is not update-permitted. | **Verified**. |
| Avatar change | `POST /api/account/avatar`; `app/app/api/account/avatar/route.ts` | `POST /files`, then `PATCH /users/me` | Current user token. Users policy permits `directus_files.create` on all fields; Users Self Access permits own `avatar` update. | Requires a configured avatar folder, <=10 MB JPEG/PNG/WebP/GIF, and recognized signature. Uploads file then links its ID. If linking fails, the uploaded file is not cleaned up. | **Verified** except folder value and folder-specific enforcement, which are **unresolved**. |
| Reauthentication | `POST /api/auth/reconfirm`; `app/app/api/auth/reconfirm/route.ts`, or login with `reauth=true` | Optional static-token `GET /users`, then `POST /auth/login` | Local credentials; username form also requires Registration `directus_users.read`. | Successful reconfirm replaces `ma_at` and sets `ma_ra` for five minutes. It does not set/replace `ma_rt`. Login's reauth path sets `ma_ra` alongside normal token cookies. Failures are generic 401. | **Verified** implementation; end-to-end username path remains unresolved. |
## Permission identity model
The export verifies these role-to-policy assignments:
- Registration Bot role -> Registration policy.
- Users role -> Users Self Access policy and Users policy, in that sort order.
- Administrator role -> Administrator policy.
- Supporter Bot role -> Supporter policy.
- Public policy has a role-less access assignment.
Because user rows and user-specific access assignments were intentionally omitted, the evidence cannot prove which identity owns any configured static credential. A deployment test must verify each credential's effective policy rather than relying on its environment-variable name.

54
directus/deployment.md Normal file
View file

@ -0,0 +1,54 @@
# Production deployment
Unless marked **inferred** or **unresolved**, facts below are verified by `reference/deployment-inventory.md`, `reference/docker-compose.sanitized.yml`, `reference/environment-variable-names.txt`, `reference/nginx-summary.txt`, or `reference/exports/settings.json`.
## Runtime and topology
| Item | Production observation |
| --- | --- |
| Directus version | 12.1.1 |
| Container image | `directus/directus:latest` |
| Compose service and container | `directus` |
| Restart policy | `unless-stopped` |
| Database client | MySQL; host, database, and credential values were intentionally omitted |
| Public URL | `https://forms.lasereverything.net` |
| Container port | Directus listens on 8055; Compose binds it to host loopback `127.0.0.1:8056` |
| Networks | Private `directus_net` bridge with IPv4/IPv6 subnets, plus external `backend_net` |
| Upload mount | Host `./uploads` to `/directus/uploads`, read/write |
| Extension mount | Host `./extensions` to `/directus/extensions`, read/write |
Production currently uses the mutable `directus/directus:latest` image tag. This document records that fact and does not change it. The running version was separately observed as 12.1.1, so a future container recreation could select a different image unless deployment controls outside the supplied evidence pin it.
The extensions directory is mounted, but its contents were not exported. Whether custom extensions are installed or active is **unresolved**.
## Reverse proxy
Nginx terminates TLS for `forms.lasereverything.net` and proxies to host loopback port 8056. It forwards the original host, client IP, forwarding chain, scheme, and WebSocket upgrade headers. Proxy read and send timeouts are 300 seconds. The request body limit is 100 MB. TLS 1.2 and 1.3 are enabled and HSTS is emitted for one year with subdomains.
The sanitized Nginx summary shows reflected-origin CORS response headers with credentials enabled and permits the common API methods and request headers. Directus-specific CORS environment variables are not explicitly set. The exact Nginx origin allow-list or surrounding condition that governs the reflected origin was not included, so whether arbitrary origins are accepted is **unresolved**.
## Mail
Directus is explicitly configured for SMTP transport. The observed categories are sender, SMTP host, port, secure mode, username, and password. Port 465 and secure mode `true` are verified; sender, host, and credential values were omitted. Delivery behavior was not tested.
The Next.js application separately references its own SMTP variable names for supporter-claim mail. Those names are documented in `env.example`; whether they point to the same SMTP service is **unresolved**.
## Authentication and security configuration
| Area | Explicit production evidence | Default or unresolved behavior |
| --- | --- | --- |
| Bootstrap administrator | `ADMIN_EMAIL` and `ADMIN_PASSWORD` names are present | Values and whether bootstrap variables still affect an initialized database are intentionally unknown |
| Signing/encryption material | `KEY` and `SECRET` names are present | Values omitted |
| Public registration | No registration environment variables are explicit; settings export has `public_registration=0` | Public `/users/register` is disabled; the app instead uses a privileged `/users` create flow |
| Registration verification | Settings export has `public_registration_verify_email=0` and no public registration role/filter | Inactive while public registration is disabled |
| Auth providers | `AUTH_PROVIDERS` is not explicitly set | Available/default providers were not exported; application code assumes local email/password auth |
| Login-attempt setting | Column layout reports a database default of 25 | The live `auth_login_attempts` value was not exported; the app also applies an in-memory 20-per-minute IP limit |
| Password policy | Column exists and defaults to null in the exported table layout | The live setting was not exported; the app enforces only an eight-character minimum on registration/change |
| CORS | No Directus CORS environment variables are explicit | Nginx adds CORS headers; effective origin policy is unresolved as noted above |
| Cookies | No Directus cookie environment variables are explicit | The app stores Directus tokens in its own `ma_at` and `ma_rt` cookies; effective Directus defaults are not proven here |
| TFA | All exported policies have `enforce_tfa=0` | Per-user TFA state was intentionally not exported |
| Policy IP restrictions | All exported policies have `ip_access=null` | No policy-level IP restriction is evidenced |
| Admin/app access | Administrator policy has both; all other exported policies have neither | Verified from policy export |
| MCP/AI/new 12.x settings | System-table columns exist | Live values were not included in the sanitized settings export and must not be inferred from column defaults |
Environment absence proves only that a setting was not explicitly supplied to the collected container. It does not prove behavior controlled by the database, an extension, another configuration source, or a Directus default that may vary by version.

71
directus/env.example Normal file
View file

@ -0,0 +1,71 @@
# Names-and-placeholders contract only. Never place production values in this file.
# Classification: public-build = embedded/exposed by Next.js; server = Next.js server only;
# deployment = Directus container/bootstrap only.
# Shared Directus location
NEXT_PUBLIC_API_BASE_URL=<public-directus-base-url> # Next.js; public-build
NEXT_PUBLIC_ASSET_URL=<public-directus-assets-base-url> # Next.js; public-build
DIRECTUS_URL=<server-directus-base-url> # Next.js; server
# Next.js privileged Directus identities
DIRECTUS_TOKEN_ADMIN_REGISTER=<registration-static-credential> # Next.js; server
DIRECTUS_SERVICE_TOKEN=<legacy-service-credential-if-still-supported> # Next.js; server
DIRECTUS_ADMIN_TOKEN=<legacy-admin-credential-if-still-supported> # Next.js; server
DIRECTUS_TOKEN_ADMIN_SUPPORTER=<supporter-static-credential> # Next.js and backfill script; server
DIRECTUS_TOKEN_SUBMIT=<settings-submission-credential> # Next.js; server
# Next.js Directus role, collection, folder, and upload configuration
DIRECTUS_ROLE_MEMBER_NAME=<registered-user-role-name> # Next.js helper; server
DIRECTUS_PROJECTS_COLLECTION=<projects-collection-name> # Next.js helper; server
DIRECTUS_AVATAR_FOLDER_ID=<avatar-folder-id> # Next.js; server
DX_FOLDER_FIBER_PHOTOS=<folder-id> # Next.js; server
DX_FOLDER_FIBER_SCREENS=<folder-id> # Next.js; server
DX_FOLDER_GALVO_PHOTOS=<folder-id> # Next.js; server
DX_FOLDER_GALVO_SCREENS=<folder-id> # Next.js; server
DX_FOLDER_GANTRY_PHOTOS=<folder-id> # Next.js; server
DX_FOLDER_GANTRY_SCREENS=<folder-id> # Next.js; server
DX_FOLDER_UV_PHOTOS=<folder-id> # Next.js; server
DX_FOLDER_UV_SCREENS=<folder-id> # Next.js; server
FILE_MAX_MB=<positive-number> # Next.js project uploads; server
SETTINGS_IMAGE_MAX_MB=<positive-number> # Next.js settings uploads; server
# Directus container identity and cryptographic configuration
KEY=<directus-key> # Directus; deployment
SECRET=<directus-secret> # Directus; deployment
PUBLIC_URL=<public-directus-base-url> # Directus; deployment
ADMIN_EMAIL=<bootstrap-admin-address> # Directus bootstrap; deployment
ADMIN_PASSWORD=<bootstrap-admin-credential> # Directus bootstrap; deployment
# Directus database
DB_CLIENT=mysql # Directus; deployment
DB_HOST=<database-host> # Directus; deployment
DB_PORT=<database-port> # Directus; deployment
DB_DATABASE=<database-name> # Directus; deployment
DB_USER=<database-user> # Directus; deployment
DB_PASSWORD=<database-credential> # Directus; deployment
DB_FILENAME=<unused-for-mysql-or-database-file> # Directus environment name observed; deployment
# Directus mail
EMAIL_TRANSPORT=smtp # Directus; deployment
EMAIL_FROM=<sender-address> # Directus and Next.js supporter mail; deployment/server
EMAIL_SMTP_HOST=<smtp-host> # Directus; deployment
EMAIL_SMTP_PORT=465 # Directus; deployment
EMAIL_SMTP_SECURE=true # Directus; deployment
EMAIL_SMTP_USER=<smtp-user> # Directus; deployment
EMAIL_SMTP_PASSWORD=<smtp-credential> # Directus; deployment
# Next.js supporter mail uses a separate naming convention
SMTP_HOST=<smtp-host> # Next.js; server
SMTP_PORT=<smtp-port> # Next.js; server
SMTP_SECURE=<true-or-false> # Next.js; server
SMTP_USER=<smtp-user> # Next.js; server
SMTP_PASS=<smtp-credential> # Next.js; server
# Runtime classification
NODE_ENV=<production-or-development> # Directus and Next.js; deployment/server
# The production Directus environment also contains runtime-provided NODE_VERSION,
# NPM_CONFIG_UPDATE_NOTIFIER, PATH, and YARN_VERSION. They are not application
# integration configuration and therefore have no configurable placeholders here.
# AUTH_PROVIDERS, Directus CORS/cookie variables, and public-registration variables
# were explicitly reported as not set and are intentionally not implied here.

47
directus/flows.md Normal file
View file

@ -0,0 +1,47 @@
# Directus flows
Two active event-triggered flows were exported. Both run with accountability `all`. The export deliberately replaces every operation's options with only `options_keys`; code, filters, collections, queries, keys, error messages, status codes, and event scopes are not available. The graphs below are therefore structural, not behavioral proofs.
## `unique_key_claims`
Description: “A flow for setting a unique key for each claim to prevent duplicates.”
```text
Run Script (exec_5dli5)
resolve -> Read Data (item_read_e4myo)
resolve -> Condition (condition_s458i)
resolve -> Throw Error (throw_error_22tpm)
reject -> end
reject -> end
reject -> end
```
- Trigger: event.
- Flow option keys: `type`, `scope`, `collections`, `return`.
- Script option key: `code`.
- Read option keys: `permissions`, `emitEvents`, `collection`, `query`.
- Condition option key: `filter`.
- Error option keys: `status`, `message`, `code`.
**Unresolved:** the triggering collection/event, how the unique key is calculated, what data is queried, the condition's polarity, error response, and whether the graph handles races atomically. The description suggests duplicate prevention, but omitted options prevent verification.
## `claim_auto_assign`
Description: “Assigns a claimant to owner and rejects other pending claims.”
```text
Condition (condition_dl7cj)
resolve -> Read Data (item_read_oyges) -> end
reject -> end
```
- Trigger: event.
- Flow option keys: `type`, `scope`, `collections`.
- Condition option key: `filter`.
- Read option keys: `permissions`, `emitEvents`, `collection`, `key`.
**Unresolved:** the triggering collection/event, condition, item key, read collection, and any mutation that performs the assignment or rejection. The exported graph contains only a condition followed by an item-read operation; no update operation is visible. The description's assignment behavior therefore cannot be reconciled with the sanitized graph without the omitted options or additional extension/hook behavior.
## Refreshing flow documentation
A future sanitized export should retain non-sensitive structural option values such as event type, collection name, and graph routing when safe, while continuing to remove credentials, destination addresses, headers, code containing secrets, and personal data. Each retained value must be reviewed rather than copied wholesale.

View file

@ -0,0 +1,61 @@
# Proposed disposable Directus integration testing
No tests described here have been implemented or run. This is a future design that must never target production.
## Environment
Use a dedicated Compose project with:
- A version-pinned Directus image matching production (currently 12.1.1), not the production `latest` deployment reference.
- A disposable MySQL database matching the production database family.
- Ephemeral uploads and a test-only extensions mount populated only when production extensions have been reviewed and are needed.
- Random per-run signing secrets and test-only static credentials.
- A loopback-only port and an unmistakable test public URL.
- Mail captured by a local sink; no external delivery.
- No production database, network, credentials, host mounts, or URLs.
Provision schema and access configuration from reviewed test fixtures derived from `schema.yaml` and the sanitized role/policy exports. Do not apply the production snapshot automatically until a reviewed fixture process accounts for system collections and version compatibility.
## Test identities
Create only synthetic records:
- Registration Bot with Registration policy and a test-only static credential.
- Supporter Bot with Supporter policy if cross-feature checks need it.
- A normal Users-role account with both Users policies.
- An administrator used only for fixture setup/teardown, never by application requests.
Have the test harness assert each identity's effective permissions before auth scenarios. This catches a valid credential attached to the wrong role/policy.
## Required scenarios
| Area | Cases and assertions |
| --- | --- |
| Registration | Valid account creates an active, login-capable Users-role account; app cookies have expected flags; omitted/default role behavior is explicit; whitespace in passwords is preserved. |
| Duplicate accounts | Duplicate email and duplicate username return the same non-enumerating conflict; mixed case behavior is recorded; failed pre-check cannot turn into a permission disclosure. |
| Email login | Correct credentials succeed; incorrect password and unknown account have indistinguishable responses; rate-limit behavior is isolated per test process. |
| Username login | Registration credential resolves username using only permitted fields; correct credentials succeed; unknown username is generic; missing/mis-scoped credential fails clearly without exposing Directus internals. |
| Current user | `/users/me` and application wrappers return only expected fields; another user's private fields cannot be queried; middleware validation accepts a valid token and rejects expired/revoked tokens. |
| Refresh | Once implemented, valid refresh rotates/returns expected tokens and cookies; expired/revoked/reused tokens fail; the endpoint never accepts an access token in place of refresh. Until then, assert/document that no application refresh endpoint exists. |
| Logout | Once strengthened, clears all auth state and invokes Directus revocation as designed. Until then, characterize the current local-only `ma_at` clearing and retained refresh cookie as a known failing contract. |
| Profile changes | Own first/last name and location succeed; email requires recent reauth; username update is denied; other-user updates are denied; the recent-auth marker expires and is single-use. |
| Password changes | Correct current password succeeds; incorrect current password fails; minimum length is enforced; username identifier path works; external-provider behavior is tested if enabled; existing token/session behavior is explicitly asserted. |
| Avatar changes | Allowed image succeeds into the test folder and links to self; bad type/signature/size fails; other-user linking and unauthorized folder behavior fail; partial upload cleanup behavior is characterized. |
| Least privilege | Registration identity can read only required role/user fields and create only permitted user fields; cannot update/delete users, read unrelated collections, administer settings, or access the Data Studio. Test whether exported sensitive read fields can be narrowed. |
## Permission-focused negative tests
- Remove Registration `directus_users.read`: username resolution and duplicate checks should fail predictably while the effect on create is separately asserted.
- Remove Registration `directus_users.create`: signup must fail without leaking permission internals.
- Attach the static credential to the wrong role to prove deployment validation catches it.
- Attempt to override role, status, policy, ID, and authentication data through the public app payload; the server must ignore or reject every override.
- Verify Users updates are limited to `id=$CURRENT_USER` and permitted fields.
- Exercise the exported `user_rigs` `%CURRENT_USER` update filter and record actual Directus 12.1.1 behavior.
## Harness and lifecycle
Run tests from outside the Next.js process against its HTTP interface, with a smaller Directus-level suite for permission diagnostics. Wait for both MySQL and Directus health, seed fixtures idempotently, run serial auth tests where rate limiting or cookie state matters, then destroy containers and volumes.
Log method, path template, status, Directus error code, and synthetic fixture ID only. Redact credentials and cookie values at the logging boundary. Never snapshot response bodies containing private profile fields.
CI should fail if the configured Directus version differs from the fixture version, a required permission assertion changes, a sensitive environment name lacks a test placeholder, or teardown leaves the disposable project running.

90
directus/permissions.md Normal file
View file

@ -0,0 +1,90 @@
# Roles, policies, and permissions
This is a readable rendering of `reference/exports/roles.json`, `policies.json`, `access.json`, and `permissions.json`. `C/R/U/D` mean create/read/update/delete. An absent action means no permission row was exported; it does not establish permissions inherited from an omitted user-specific policy.
## Role and policy assignments
| Role/access subject | Assigned policy | Admin access | Data Studio app access | Enforced TFA | IP restriction |
| --- | --- | --- | --- | --- | --- |
| Administrator | Administrator | Yes | Yes | No | None |
| Registration Bot | Registration | No | No | No | None |
| Supporter Bot | Supporter | No | No | No | None |
| Users | Users Self Access (sort 1), Users (sort 2) | No | No | No | None |
| Public/role-less | Public | No | No | No | None |
No user-specific access assignments appear in the sanitized export because user data was intentionally omitted. The Administrator policy's collection permissions are implicit in `admin_access`; no individual rows were exported for it.
## High-level matrix
| Collection group | Public | Users | Users Self Access | Registration | Supporter |
| --- | --- | --- | --- | --- | --- |
| Public catalog/content collections listed below | R | R | - | - | - |
| `directus_fields` | R: `id,collection,field` | R: metadata fields listed below | - | - | - |
| `directus_files` | R all fields | C/R all fields | - | - | - |
| `directus_users` | - | R public profile fields | own-row R/U | R/C broad registration fields | - |
| Projects and settings collections | R | C/R/U/D | - | - | - |
| `projects_files` | R | C/R | - | - | - |
| `user_claims` | - | C/R/U/D with presets and own/pending filters | - | - | - |
| `user_rigs` | - | C/R/U/D with owner controls | - | - | - |
| `user_rig_type` | - | C/R | - | - | - |
| `user_memberships` | - | - | - | - | C/R/U all fields |
| `directus_roles` | - | - | - | R selected role fields | - |
## Public policy
Unrestricted read, all fields, is exported for:
`bg_cat`, `bg_entries`, `bg_sub_cat`, `directus_files`, `hazard_danger`, `hazard_severity`, `hazard_source`, `hazard_tags`, `laser_focus_lens`, `laser_focus_lens_config`, `laser_focus_lens_diameter`, `laser_scan_lens`, `laser_scan_lens_apt`, `laser_scan_lens_config`, `laser_scan_lens_exp`, `laser_software`, `laser_source`, `material`, `material_cat`, `material_coating`, `material_coating_hazard_tags`, `material_color`, `material_hazard_tags`, `material_opacity`, `material_status`, `projects`, `projects_files`, `settings_co2gal`, `settings_co2gan`, `settings_fiber`, and `settings_uv`.
Public can also read `directus_fields`, restricted to `field,collection,id`. There are no exported public create, update, or delete permissions.
## Users policy
Users have unrestricted, all-field read for every public content collection above. Additional or expanded permissions are:
| Collection | Actions | Row filter | Fields/presets |
| --- | --- | --- | --- |
| `directus_fields` | R | None | `id,collection,field,special,interface,options,readonly,hidden,required,sort,width,group,note` |
| `directus_files` | C/R | None | All fields; no folder/owner filter or preset exported |
| `directus_users` | R | None | `username,first_name,avatar,location,id` |
| `projects` | C/R/U/D | U/D require `owner=$CURRENT_USER`; C/R unrestricted | All fields; C presets `owner` and `uploader` to `$CURRENT_USER` |
| `projects_files` | C/R | None | All fields; no ownership filter exported |
| `settings_co2gal` | C/R/U/D | U/D require `owner=$CURRENT_USER`; C/R unrestricted | All fields; C presets owner/uploader to current user |
| `settings_co2gan` | C/R/U/D | Same as above | Same as above |
| `settings_fiber` | C/R/U/D | Same as above | Same as above |
| `settings_uv` | C/R/U/D | U/D require `owner=$CURRENT_USER`; C/R unrestricted | C/R/U all fields and owner/uploader presets on C; D has `fields=null` in the export |
| `user_claims` | C/R/U/D | R: claimant is current user. U/D: claimant is current user and status is `pending`. C unrestricted. | C fields omit `claimant` and `status` but preset them to current user/`pending`; R selected fields; U only `note`; D `fields=null` |
| `user_rigs` | C/R/U/D | R/D require owner current user. U filter contains `owner _eq "%CURRENT_USER"` exactly as exported. C unrestricted. | All fields; C presets owner to `$CURRENT_USER` |
| `user_rig_type` | C/R | None | All fields |
The `%CURRENT_USER` update filter on `user_rigs` differs from the `$CURRENT_USER` token used everywhere else. This is a **verified exported value** and is likely ineffective, but its Directus runtime interpretation has not been tested.
No validation rules are present on any exported Users permission row.
## Users Self Access policy
Both rows require `id=$CURRENT_USER`:
- Read fields: `username,last_name,first_name,email,password,avatar,location,title,description,tags,language,tfa_secret,email_notifications,id`.
- Update fields: `first_name,last_name,email,password,avatar,location`.
There are no presets or validation rules. Username is readable but not updateable. The exported read list includes sensitive field names such as `password` and `tfa_secret`; Directus may suppress stored secret values independently, but that behavior is **not proven by this export** and should be tested.
## Registration policy
- `directus_users.create`: unrestricted rows; fields `email,username,password,role,status,auth_data,first_name,last_name,avatar,id`.
- `directus_users.read`: unrestricted rows; a broad field list including `username,email,id,password,auth_data,token,policies,role,status,tfa_secret` and UI/theme fields.
- `directus_roles.read`: unrestricted rows; fields `id,name,icon,description,parent,children`.
No validation rules or presets are exported. Least privilege therefore depends on the static credential being held only by the server and the application strictly controlling submitted fields. The application registration route currently uses email, username, password, and optionally role; username-to-email login resolution uses read access.
## Supporter policy
`user_memberships` has unrestricted create, read, and update permissions on all fields. No delete row, row filters, validation rules, or presets are exported. The policy is assigned to Supporter Bot.
## Validation and preset summary
- Every exported `validation` value is null.
- Presets exist only for Users creates on projects/settings (`owner`/`uploader`), user claims (`claimant`/`pending`), and user rigs (`owner`).
- Row filters exist only for Users updates/deletes and selected reads described above, plus own-row filters in Users Self Access.
- Null permissions mean no row filter in these exports; null fields on delete rows are preserved as observed rather than interpreted as all/no fields.

View file

@ -0,0 +1,29 @@
# Directus Deployment Inventory
Generated from the running production container.
- Directus version: 12.1.1
- Container image: directus/directus:latest
- Public URL: https://forms.lasereverything.net
- Host binding: 127.0.0.1:8056
- Database client: mysql
- Database service and database names: intentionally omitted
- Email transport: smtp
- SMTP port: 465
- SMTP secure mode: true
- Explicit AUTH_PROVIDERS variable: not set
- Explicit CORS variables: not set
- Explicit cookie variables: not set
- Explicit registration environment variables: not set
## Persistent mounts
- `/directus/extensions` is mounted from the host, mode `rw`
- `/directus/uploads` is mounted from the host, mode `rw`
## Notes
- Secret values are intentionally excluded.
- Registration behavior also depends on fields stored in the Directus settings table.
- Role, policy, and permission exports are stored separately.
- Flow option values are excluded because they may contain credentials, headers, addresses, or other sensitive configuration.

View file

@ -0,0 +1,28 @@
services:
directus:
image: directus/directus:latest
container_name: directus
ports:
- "127.0.0.1:8056:8055"
env_file:
- .env
volumes:
- ./uploads:/directus/uploads
- ./extensions:/directus/extensions
networks:
- default
- backend_net
restart: unless-stopped
networks:
default:
name: directus_net
driver: bridge
enable_ipv6: true
ipam:
config:
- subnet: 10.40.0.0/16
- subnet: fd00:40::/64
backend_net:
external: true

View file

@ -0,0 +1,24 @@
ADMIN_EMAIL
ADMIN_PASSWORD
DB_CLIENT
DB_DATABASE
DB_FILENAME
DB_HOST
DB_PASSWORD
DB_PORT
DB_USER
EMAIL_FROM
EMAIL_SMTP_HOST
EMAIL_SMTP_PASSWORD
EMAIL_SMTP_PORT
EMAIL_SMTP_SECURE
EMAIL_SMTP_USER
EMAIL_TRANSPORT
KEY
NODE_ENV
NODE_VERSION
NPM_CONFIG_UPDATE_NOTIFIER
PATH
PUBLIC_URL
SECRET
YARN_VERSION

View file

@ -0,0 +1,44 @@
[
{
"id": "45b5000f-f495-4940-935a-d4bf2e409844",
"role": "a6d3c757-a96b-4f0c-b96b-a6109d60a7ba",
"policy": "93fc7a9e-16de-45d4-89b1-9fa129e81442",
"sort": null,
"user": null
},
{
"id": "2ddbc9da-e0f5-4dff-a952-c3d7586ab706",
"role": "296a28bc-60ab-4251-8bef-27f6dfb67948",
"policy": "80b3902c-06e6-480c-9d29-d2a46552f60e",
"sort": 1,
"user": null
},
{
"id": "767277dd-3b08-4d72-9bde-437af91e1d88",
"role": "a2a695be-88f9-4948-b2a4-e0e22b01b32e",
"policy": "2e4acc56-7f7e-4a10-9775-1c0dd9f47ded",
"sort": 1,
"user": null
},
{
"id": "abfa5434-1ace-4e1f-9a93-7d84aabb0a95",
"role": "00e518e3-75b3-4c6f-b57d-51a4f1af4781",
"policy": "494e10a0-03ec-40b9-a7a5-c3a1e2b95001",
"sort": 1,
"user": null
},
{
"id": "c1480ea2-ca4d-46c9-8034-09b76d0a41cf",
"role": null,
"policy": "abf8a154-5b1c-4a46-ac9c-7300570f4f17",
"sort": 1,
"user": null
},
{
"id": "cf8e40bd-3fbe-4df5-a845-01802a3e7e1c",
"role": "296a28bc-60ab-4251-8bef-27f6dfb67948",
"policy": "536f020c-4f05-497d-8a6c-54e13f62c694",
"sort": 2,
"user": null
}
]

View file

@ -0,0 +1,19 @@
[
{
"field": "username",
"special": null,
"interface": "input",
"options": null,
"display": "raw",
"display_options": null,
"readonly": 0,
"hidden": 0,
"required": 1,
"sort": 1,
"width": "full",
"note": null,
"conditions": null,
"validation": null,
"validation_message": null
}
]

View file

@ -0,0 +1,35 @@
[
{
"id": "fdc2d675-e365-4431-9cd0-27e571c89e04",
"name": "claim_auto_assign",
"icon": "bolt",
"color": null,
"description": "Assigns a claimant to owner and rejects other pending claims.",
"status": "active",
"trigger": "event",
"accountability": "all",
"options_keys": [
"type",
"scope",
"collections"
],
"root_operation": "e19723a6-e75c-4bc8-92aa-be06bb0528af"
},
{
"id": "99cc3ea5-3ab7-431f-a4f6-ce4e7677b092",
"name": "unique_key_claims",
"icon": "bolt",
"color": null,
"description": "A flow for setting a unique key for each claim to prevent duplicates.",
"status": "active",
"trigger": "event",
"accountability": "all",
"options_keys": [
"type",
"scope",
"collections",
"return"
],
"root_operation": "2245466f-8178-4e5c-b058-819eb421de47"
}
]

View file

@ -0,0 +1,94 @@
[
{
"id": "03c6582f-800d-4e94-9fea-f77a9ea5e3aa",
"name": "Condition",
"key": "condition_s458i",
"type": "condition",
"position_x": 55,
"position_y": 1,
"resolve": "d4266993-ceae-452a-9b7d-664c11bf1013",
"reject": null,
"flow": "99cc3ea5-3ab7-431f-a4f6-ce4e7677b092",
"options_keys": [
"filter"
]
},
{
"id": "cb7101fc-e2c5-4cfc-b1d6-486ac70d286f",
"name": "Read Data",
"key": "item_read_e4myo",
"type": "item-read",
"position_x": 37,
"position_y": 1,
"resolve": "03c6582f-800d-4e94-9fea-f77a9ea5e3aa",
"reject": null,
"flow": "99cc3ea5-3ab7-431f-a4f6-ce4e7677b092",
"options_keys": [
"permissions",
"emitEvents",
"collection",
"query"
]
},
{
"id": "2245466f-8178-4e5c-b058-819eb421de47",
"name": "Run Script",
"key": "exec_5dli5",
"type": "exec",
"position_x": 19,
"position_y": 1,
"resolve": "cb7101fc-e2c5-4cfc-b1d6-486ac70d286f",
"reject": null,
"flow": "99cc3ea5-3ab7-431f-a4f6-ce4e7677b092",
"options_keys": [
"code"
]
},
{
"id": "d4266993-ceae-452a-9b7d-664c11bf1013",
"name": "Throw Error",
"key": "throw_error_22tpm",
"type": "throw-error",
"position_x": 73,
"position_y": 1,
"resolve": null,
"reject": null,
"flow": "99cc3ea5-3ab7-431f-a4f6-ce4e7677b092",
"options_keys": [
"status",
"message",
"code"
]
},
{
"id": "e19723a6-e75c-4bc8-92aa-be06bb0528af",
"name": "Condition",
"key": "condition_dl7cj",
"type": "condition",
"position_x": 19,
"position_y": 1,
"resolve": "ebd0e644-1be0-4200-89c4-844b06c7d788",
"reject": null,
"flow": "fdc2d675-e365-4431-9cd0-27e571c89e04",
"options_keys": [
"filter"
]
},
{
"id": "ebd0e644-1be0-4200-89c4-844b06c7d788",
"name": "Read Data",
"key": "item_read_oyges",
"type": "item-read",
"position_x": 37,
"position_y": 1,
"resolve": null,
"reject": null,
"flow": "fdc2d675-e365-4431-9cd0-27e571c89e04",
"options_keys": [
"permissions",
"emitEvents",
"collection",
"key"
]
}
]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,62 @@
[
{
"id": "abf8a154-5b1c-4a46-ac9c-7300570f4f17",
"name": "$t:public_label",
"icon": "public",
"description": "$t:public_description",
"admin_access": 0,
"app_access": 0,
"enforce_tfa": 0,
"ip_access": null
},
{
"id": "93fc7a9e-16de-45d4-89b1-9fa129e81442",
"name": "Administrator",
"icon": "verified",
"description": "$t:admin_description",
"admin_access": 1,
"app_access": 1,
"enforce_tfa": 0,
"ip_access": null
},
{
"id": "494e10a0-03ec-40b9-a7a5-c3a1e2b95001",
"name": "Registration",
"icon": "robot",
"description": "Registration Bot",
"admin_access": 0,
"app_access": 0,
"enforce_tfa": 0,
"ip_access": null
},
{
"id": "2e4acc56-7f7e-4a10-9775-1c0dd9f47ded",
"name": "Supporter",
"icon": "robot_2",
"description": "Supporter Bot",
"admin_access": 0,
"app_access": 0,
"enforce_tfa": 0,
"ip_access": null
},
{
"id": "536f020c-4f05-497d-8a6c-54e13f62c694",
"name": "Users",
"icon": "badge",
"description": "Registered Users",
"admin_access": 0,
"app_access": 0,
"enforce_tfa": 0,
"ip_access": null
},
{
"id": "80b3902c-06e6-480c-9d29-d2a46552f60e",
"name": "Users Self Access",
"icon": "badge",
"description": "Registered Users Self Access",
"admin_access": 0,
"app_access": 0,
"enforce_tfa": 0,
"ip_access": null
}
]

View file

@ -0,0 +1,30 @@
[
{
"id": "a6d3c757-a96b-4f0c-b96b-a6109d60a7ba",
"name": "Administrator",
"icon": "verified",
"description": "$t:admin_description",
"parent": null
},
{
"id": "00e518e3-75b3-4c6f-b57d-51a4f1af4781",
"name": "Registration Bot",
"icon": "robot",
"description": "Registers accounts.",
"parent": null
},
{
"id": "a2a695be-88f9-4948-b2a4-e0e22b01b32e",
"name": "Supporter Bot",
"icon": "supervised_user_circle",
"description": "Registers supporters.",
"parent": null
},
{
"id": "296a28bc-60ab-4251-8bef-27f6dfb67948",
"name": "Users",
"icon": "accessibility_new",
"description": "Registered users.",
"parent": null
}
]

View file

@ -0,0 +1,11 @@
[
{
"project_name": "Directus",
"project_url": null,
"default_language": "en-US",
"public_registration": 0,
"public_registration_verify_email": 0,
"public_registration_role": null,
"public_registration_email_filter": null
}
]

View file

@ -0,0 +1,33 @@
3: listen 80;
4: server_name forms.lasereverything.net;
5: client_max_body_size 100M;
8: location /.well-known/acme-challenge/ {
13: location / {
20: listen 443 ssl;
22: server_name forms.lasereverything.net;
23: client_max_body_size 100M;
28: ssl_protocols TLSv1.2 TLSv1.3;
31: add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
33: location / {
34: proxy_pass http://127.0.0.1:8056;
37: proxy_set_header Host $host;
38: proxy_set_header X-Real-IP $remote_addr;
39: proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
40: proxy_set_header X-Forwarded-Proto $scheme;
43: proxy_set_header Upgrade $http_upgrade;
44: proxy_set_header Connection "upgrade";
46: proxy_read_timeout 300;
48: proxy_send_timeout 300;
57: add_header Access-Control-Allow-Origin "$http_origin" always;
58: add_header Access-Control-Allow-Credentials "true" always;
59: add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With" always;
60: add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
61: add_header Vary "Origin" always;
66: add_header Access-Control-Max-Age 1728000 always;
67: add_header Content-Type "text/plain; charset=utf-8" always;
68: add_header Content-Length 0 always;
71: add_header Access-Control-Allow-Origin "$http_origin" always;
72: add_header Access-Control-Allow-Credentials "true" always;
73: add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With" always;
74: add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
75: add_header Vary "Origin" always;

View file

@ -0,0 +1,8 @@
Live schema SHA-256:
709c5f5fa6c239406085e8c7e5959d2d096d36ad1d8676727a0ca473536498e6 /root/directus-codex-context-20260710-102253/schema.yaml
Existing repository schema SHA-256:
8e21cd555b297e729a39f387eadbf4d32621c986516c9d3cc896b01d02efdf8b /srv/codex-work/lasereverything.net.db/app/schema.yaml
Comparison:
The live and repository schema files differ.

View file

@ -0,0 +1,5 @@
id char(36) NO PRI NULL
role char(36) YES MUL NULL
user char(36) YES MUL NULL
policy char(36) NO MUL NULL
sort int(11) YES NULL
1 id char(36) NO PRI NULL
2 role char(36) YES MUL NULL
3 user char(36) YES MUL NULL
4 policy char(36) NO MUL NULL
5 sort int(11) YES NULL

View file

@ -0,0 +1,20 @@
id int(10) unsigned NO PRI NULL auto_increment
collection varchar(64) NO MUL NULL
field varchar(64) NO NULL
special varchar(64) YES NULL
interface varchar(64) YES NULL
options longtext YES NULL
display varchar(64) YES NULL
display_options longtext YES NULL
readonly tinyint(1) NO 0
hidden tinyint(1) NO 0
sort int(10) unsigned YES NULL
width varchar(30) YES full
translations longtext YES NULL
note text YES NULL
conditions longtext YES NULL
required tinyint(1) YES 0
group varchar(64) YES NULL
validation longtext YES NULL
validation_message text YES NULL
searchable tinyint(1) NO 1
1 id int(10) unsigned NO PRI NULL auto_increment
2 collection varchar(64) NO MUL NULL
3 field varchar(64) NO NULL
4 special varchar(64) YES NULL
5 interface varchar(64) YES NULL
6 options longtext YES NULL
7 display varchar(64) YES NULL
8 display_options longtext YES NULL
9 readonly tinyint(1) NO 0
10 hidden tinyint(1) NO 0
11 sort int(10) unsigned YES NULL
12 width varchar(30) YES full
13 translations longtext YES NULL
14 note text YES NULL
15 conditions longtext YES NULL
16 required tinyint(1) YES 0
17 group varchar(64) YES NULL
18 validation longtext YES NULL
19 validation_message text YES NULL
20 searchable tinyint(1) NO 1

View file

@ -0,0 +1,12 @@
id char(36) NO PRI NULL
name varchar(255) NO NULL
icon varchar(64) YES NULL
color varchar(255) YES NULL
description text YES NULL
status varchar(255) NO active
trigger varchar(255) YES NULL
accountability varchar(255) YES all
options longtext YES NULL
operation char(36) YES UNI NULL
date_created timestamp YES current_timestamp()
user_created char(36) YES MUL NULL
1 id char(36) NO PRI NULL
2 name varchar(255) NO NULL
3 icon varchar(64) YES NULL
4 color varchar(255) YES NULL
5 description text YES NULL
6 status varchar(255) NO active
7 trigger varchar(255) YES NULL
8 accountability varchar(255) YES all
9 options longtext YES NULL
10 operation char(36) YES UNI NULL
11 date_created timestamp YES current_timestamp()
12 user_created char(36) YES MUL NULL

View file

@ -0,0 +1,12 @@
id char(36) NO PRI NULL
name varchar(255) YES NULL
key varchar(255) NO NULL
type varchar(255) NO NULL
position_x int(11) NO NULL
position_y int(11) NO NULL
options longtext YES NULL
resolve char(36) YES UNI NULL
reject char(36) YES UNI NULL
flow char(36) NO MUL NULL
date_created timestamp YES current_timestamp()
user_created char(36) YES MUL NULL
1 id char(36) NO PRI NULL
2 name varchar(255) YES NULL
3 key varchar(255) NO NULL
4 type varchar(255) NO NULL
5 position_x int(11) NO NULL
6 position_y int(11) NO NULL
7 options longtext YES NULL
8 resolve char(36) YES UNI NULL
9 reject char(36) YES UNI NULL
10 flow char(36) NO MUL NULL
11 date_created timestamp YES current_timestamp()
12 user_created char(36) YES MUL NULL

View file

@ -0,0 +1,8 @@
id int(10) unsigned NO PRI NULL auto_increment
collection varchar(64) NO MUL NULL
action varchar(10) NO NULL
permissions longtext YES NULL
validation longtext YES NULL
presets longtext YES NULL
fields text YES NULL
policy char(36) NO MUL NULL
1 id int(10) unsigned NO PRI NULL auto_increment
2 collection varchar(64) NO MUL NULL
3 action varchar(10) NO NULL
4 permissions longtext YES NULL
5 validation longtext YES NULL
6 presets longtext YES NULL
7 fields text YES NULL
8 policy char(36) NO MUL NULL

View file

@ -0,0 +1,8 @@
id char(36) NO PRI NULL
name varchar(100) NO NULL
icon varchar(64) NO badge
description text YES NULL
ip_access text YES NULL
enforce_tfa tinyint(1) NO 0
admin_access tinyint(1) NO 0
app_access tinyint(1) NO 0
1 id char(36) NO PRI NULL
2 name varchar(100) NO NULL
3 icon varchar(64) NO badge
4 description text YES NULL
5 ip_access text YES NULL
6 enforce_tfa tinyint(1) NO 0
7 admin_access tinyint(1) NO 0
8 app_access tinyint(1) NO 0

View file

@ -0,0 +1,5 @@
id char(36) NO PRI NULL
name varchar(100) NO NULL
icon varchar(64) NO supervised_user_circle
description text YES NULL
parent char(36) YES MUL NULL
1 id char(36) NO PRI NULL
2 name varchar(100) NO NULL
3 icon varchar(64) NO supervised_user_circle
4 description text YES NULL
5 parent char(36) YES MUL NULL

View file

@ -0,0 +1,66 @@
id int(10) unsigned NO PRI NULL auto_increment
project_name varchar(100) NO Directus
project_url varchar(255) YES NULL
project_color varchar(255) NO #6644FF
project_logo char(36) YES MUL NULL
public_foreground char(36) YES MUL NULL
public_background char(36) YES MUL NULL
public_note text YES NULL
auth_login_attempts int(10) unsigned YES 25
auth_password_policy varchar(100) YES NULL
storage_asset_transform varchar(7) YES all
storage_asset_presets longtext YES NULL
custom_css text YES NULL
storage_default_folder char(36) YES MUL NULL
basemaps longtext YES NULL
mapbox_key varchar(255) YES NULL
module_bar longtext YES NULL
project_descriptor varchar(100) YES NULL
default_language varchar(255) NO en-US
custom_aspect_ratios longtext YES NULL
public_favicon char(36) YES MUL NULL
default_appearance varchar(255) NO auto
default_theme_light varchar(255) YES NULL
theme_light_overrides longtext YES NULL
default_theme_dark varchar(255) YES NULL
theme_dark_overrides longtext YES NULL
report_error_url varchar(255) YES NULL
report_bug_url varchar(255) YES NULL
report_feature_url varchar(255) YES NULL
public_registration tinyint(1) NO 0
public_registration_verify_email tinyint(1) NO 1
public_registration_role char(36) YES MUL NULL
public_registration_email_filter longtext YES NULL
visual_editor_urls longtext YES NULL
project_id char(36) YES NULL
mcp_enabled tinyint(1) NO 0
mcp_allow_deletes tinyint(1) NO 0
mcp_prompts_collection varchar(255) YES NULL
mcp_system_prompt_enabled tinyint(1) NO 1
mcp_system_prompt text YES NULL
project_owner varchar(255) YES NULL
project_usage varchar(255) YES NULL
org_name varchar(255) YES NULL
product_updates tinyint(1) YES NULL
project_status varchar(255) YES NULL
ai_openai_api_key text YES NULL
ai_anthropic_api_key text YES NULL
ai_system_prompt text YES NULL
ai_google_api_key text YES NULL
ai_openai_compatible_api_key text YES NULL
ai_openai_compatible_base_url text YES NULL
ai_openai_compatible_name text YES NULL
ai_openai_compatible_models longtext YES NULL
ai_openai_compatible_headers longtext YES NULL
ai_openai_allowed_models longtext YES NULL
ai_anthropic_allowed_models longtext YES NULL
ai_google_allowed_models longtext YES NULL
collaborative_editing_enabled tinyint(1) NO 0
ai_translation_default_model text YES NULL
ai_translation_glossary longtext YES NULL
ai_translation_style_guide text YES NULL
license_key varchar(255) YES NULL
license_token text YES NULL
mcp_oauth_enabled tinyint(1) NO 0
mcp_oauth_dcr_enabled tinyint(1) NO 0
mcp_oauth_cimd_enabled tinyint(1) NO 0
1 id int(10) unsigned NO PRI NULL auto_increment
2 project_name varchar(100) NO Directus
3 project_url varchar(255) YES NULL
4 project_color varchar(255) NO #6644FF
5 project_logo char(36) YES MUL NULL
6 public_foreground char(36) YES MUL NULL
7 public_background char(36) YES MUL NULL
8 public_note text YES NULL
9 auth_login_attempts int(10) unsigned YES 25
10 auth_password_policy varchar(100) YES NULL
11 storage_asset_transform varchar(7) YES all
12 storage_asset_presets longtext YES NULL
13 custom_css text YES NULL
14 storage_default_folder char(36) YES MUL NULL
15 basemaps longtext YES NULL
16 mapbox_key varchar(255) YES NULL
17 module_bar longtext YES NULL
18 project_descriptor varchar(100) YES NULL
19 default_language varchar(255) NO en-US
20 custom_aspect_ratios longtext YES NULL
21 public_favicon char(36) YES MUL NULL
22 default_appearance varchar(255) NO auto
23 default_theme_light varchar(255) YES NULL
24 theme_light_overrides longtext YES NULL
25 default_theme_dark varchar(255) YES NULL
26 theme_dark_overrides longtext YES NULL
27 report_error_url varchar(255) YES NULL
28 report_bug_url varchar(255) YES NULL
29 report_feature_url varchar(255) YES NULL
30 public_registration tinyint(1) NO 0
31 public_registration_verify_email tinyint(1) NO 1
32 public_registration_role char(36) YES MUL NULL
33 public_registration_email_filter longtext YES NULL
34 visual_editor_urls longtext YES NULL
35 project_id char(36) YES NULL
36 mcp_enabled tinyint(1) NO 0
37 mcp_allow_deletes tinyint(1) NO 0
38 mcp_prompts_collection varchar(255) YES NULL
39 mcp_system_prompt_enabled tinyint(1) NO 1
40 mcp_system_prompt text YES NULL
41 project_owner varchar(255) YES NULL
42 project_usage varchar(255) YES NULL
43 org_name varchar(255) YES NULL
44 product_updates tinyint(1) YES NULL
45 project_status varchar(255) YES NULL
46 ai_openai_api_key text YES NULL
47 ai_anthropic_api_key text YES NULL
48 ai_system_prompt text YES NULL
49 ai_google_api_key text YES NULL
50 ai_openai_compatible_api_key text YES NULL
51 ai_openai_compatible_base_url text YES NULL
52 ai_openai_compatible_name text YES NULL
53 ai_openai_compatible_models longtext YES NULL
54 ai_openai_compatible_headers longtext YES NULL
55 ai_openai_allowed_models longtext YES NULL
56 ai_anthropic_allowed_models longtext YES NULL
57 ai_google_allowed_models longtext YES NULL
58 collaborative_editing_enabled tinyint(1) NO 0
59 ai_translation_default_model text YES NULL
60 ai_translation_glossary longtext YES NULL
61 ai_translation_style_guide text YES NULL
62 license_key varchar(255) YES NULL
63 license_token text YES NULL
64 mcp_oauth_enabled tinyint(1) NO 0
65 mcp_oauth_dcr_enabled tinyint(1) NO 0
66 mcp_oauth_cimd_enabled tinyint(1) NO 0

View file

@ -0,0 +1,55 @@
# Production versus older schema snapshot
Compared files:
- Current production evidence: `directus/schema.yaml`, Directus 12.1.1, MySQL.
- Older repository snapshot: `app/schema.yaml`, Directus 11.12.0, MySQL.
The supplied hashes differ. A structural YAML comparison found 36 versus 34 collection entries, 394 versus 379 field entries, and 138 versus 136 relation entries. Neither snapshot was modified.
## Meaningful additions
### `directus_users` collection metadata
The current snapshot includes `directus_users` as a collection with display template `{{ username }} ({{ email }})`. The older snapshot contains the custom `directus_users.username` field and relations to the system collection but omits the collection entry itself.
The username field remains a unique nullable `varchar(255)` at the database layer while Directus metadata marks it required, visible, editable, and searchable. The older snapshot has the same database and required-field characteristics; current metadata additionally contains the Directus 12 `searchable` property.
### `user_memberships`
Production adds the `user_memberships` collection with 15 fields:
- Integer auto-increment primary key: `id`.
- Provider/account data: `provider`, `external_user_id`, `email`, `username`, `status`, `tier`.
- Timing: `started_at`, `renews_at`, `last_event_at`.
- Claim/support data: `claim_token`, `claim_expires_at`, `raw`.
- User relations: `app_user`, `claim_user_id`.
`app_user` and `claim_user_id` are nullable indexed M2O foreign keys to `directus_users.id`; both use `SET NULL` on delete and `RESTRICT` on update. The schema does not mark `claim_token` unique. Provider choices are Ko-Fi, Patreon, and LMA/Mighty; status choices are active, canceled, refunded, and one-time. These are metadata choices, not database constraints.
### Settings raster metadata
The `settings_co2gal.raster_settings` repeater gained three nested metadata fields: `angle` (integer), `auto` (boolean), and `increment` (integer). These are changes inside the JSON/repeater field definition, not new top-level database columns.
### Display metadata
- `settings_co2gal.owner` changed from no display renderer to related-values with template `{{username}}`.
- `bg_entries.scores` changed its display formatting template from `{{ cat }}` to `{{ body }}`.
## Version-generated metadata differences
Most raw differences are serialization changes rather than business-schema changes:
- All 34 pre-existing collection entries gained `meta.autosave_revision_interval: null` and `meta.status: active`.
- All 379 pre-existing field entries gained `meta.searchable: true`.
These additions are consistent across the snapshots and correspond to newer snapshot metadata emitted by Directus 12. They do not by themselves show database-column changes.
## Unchanged structural areas
- No older collection, field, or relation key was removed.
- All 136 relations present in the older snapshot compare equal structurally.
- The two added relations are the new `user_memberships` links described above.
- Database vendor remains MySQL and snapshot format version remains 1.
This comparison is structural and does not compare data, permissions, policies, flows, settings-table values, indexes not represented by the snapshot, extension behavior, or migration history.

22571
directus/schema.yaml Normal file

File diff suppressed because it is too large Load diff