From eb3b85f36361c90f16bdcd7036b2b6d4b5fa9ab4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 26 Feb 2026 06:35:25 +0000 Subject: [PATCH 1/6] docs: add Cursor Cloud specific instructions to AGENTS.md --- AGENTS.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 634cc81..4b2e215 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ > Scope: Root project (applies to all subdirectories unless overridden) -Centralized identity and hub management system for the AllThingsLinux (ATL) community. One ATL identity provisions access to all services: email, IRC, XMPP, SSH, web hosting, Discord, wiki access, and developer tools across `atl.dev`, `atl.sh`, `atl.tools`, and `atl.chat`. +Centralized identity and hub management system for the AllThingsLinux (ATL) community. One ATL identity provisions access to all services: email, IRC, XMPP, SSH, web hosting, Discord, wiki access, and developer tools across `atl.dev`, `atl.sh`, `[REDACTED]`, and `atl.chat`. ## Quick Facts @@ -131,6 +131,36 @@ Each module has its own `keys.ts` file using `@t3-oss/env-nextjs`. The central ` - [Code Standards](.agents/code-standards.md) — Rules beyond what Biome enforces - [Project Skills](.agents/skills.md) — Available agent skills index +## Cursor Cloud specific instructions + +### Services + +| Service | How to start | Port | Required? | +|---------|-------------|------|-----------| +| PostgreSQL 18 | `docker compose up -d portal-db` | 5432 | Yes | +| Next.js dev server | `pnpm dev` | 3000 | Yes | + +### Environment setup + +A `.env` file at the project root is required with at minimum `DATABASE_URL`, `BETTER_AUTH_SECRET`, and `BETTER_AUTH_URL`. See the README "Database Setup" and "Getting Started" sections for connection details and defaults. + +All integration env vars (Discord, IRC, XMPP, Mailcow, Sentry) are optional — the app starts without them. + +### Startup sequence + +1. Ensure Docker daemon is running (`sudo dockerd` if not already started) +2. `docker compose up -d portal-db` — start PostgreSQL +3. Wait for healthy: `docker compose ps` should show `(healthy)` +4. `pnpm db:push` — sync schema to dev DB (safe for dev; use `pnpm db:migrate` for prod-like flow) +5. `pnpm dev` — start the dev server on port 3000 + +### Gotchas + +- **`pnpm typegen` before `pnpm type-check`**: Next.js 16 generates `RouteContext` types via `next typegen`. Running `tsc --noEmit` without it first produces `TS2304: Cannot find name 'RouteContext'` errors. The `pnpm build` script runs typegen automatically, but `pnpm type-check` does not. +- **Docker-in-Docker**: The Cloud VM runs inside a container. Docker requires `fuse-overlayfs` storage driver, `iptables-legacy`, and the daemon started via `sudo dockerd`. See the environment snapshot for pre-installed Docker. +- **`pg-native` build script**: The `libpq` native build is not in `onlyBuiltDependencies`. The app falls back to the pure-JS `pg` driver, which works fine for development. +- **Pre-existing test failures**: 2 test files (`tests/app/api/admin/irc-accounts/route.test.ts` and `tests/app/api/bridge/identity/route.test.ts`) fail due to `vi.mock` hoisting issues with t3-env server-side variable access. These are not environment issues — all 118 individual tests pass. + ## Finish the Task - [ ] Run `pnpm fix` before committing. From d53448e232b06004c598bf8deeccce66f958dab4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 26 Feb 2026 06:59:02 +0000 Subject: [PATCH 2/6] perf: parallelize independent DB queries with Promise.all and use toSorted - admin/stats: parallelize 4 independent stat queries (users, sessions, api-keys, oauth-clients) - admin/users: parallelize data fetch and count query - admin/irc-accounts: parallelize data fetch and count query - admin/xmpp-accounts: parallelize data fetch and count query - admin/mailcow-accounts: parallelize data fetch and count query - routing/permissions: use .toSorted() instead of .sort() to avoid mutating source arrays Applied Vercel React Best Practices (async-parallel, js-tosorted-immutable). --- src/app/api/admin/irc-accounts/route.ts | 38 ++++++------- src/app/api/admin/mailcow-accounts/route.ts | 38 ++++++------- src/app/api/admin/stats/route.ts | 62 ++++++++++----------- src/app/api/admin/users/route.ts | 25 +++++---- src/app/api/admin/xmpp-accounts/route.ts | 38 ++++++------- src/features/routing/lib/permissions.ts | 4 +- 6 files changed, 98 insertions(+), 107 deletions(-) diff --git a/src/app/api/admin/irc-accounts/route.ts b/src/app/api/admin/irc-accounts/route.ts index 633b187..d2d116b 100644 --- a/src/app/api/admin/irc-accounts/route.ts +++ b/src/app/api/admin/irc-accounts/route.ts @@ -43,26 +43,24 @@ export async function GET(request: NextRequest) { } const whereClause = conditions.length > 0 ? and(...conditions) : undefined; - const rows = await db - .select({ - ircAccount, - user: { - id: user.id, - email: user.email, - name: user.name, - }, - }) - .from(ircAccount) - .leftJoin(user, eq(ircAccount.userId, user.id)) - .where(whereClause) - .orderBy(desc(ircAccount.createdAt)) - .limit(limit) - .offset(offset); - - const [totalResult] = await db - .select({ count: count() }) - .from(ircAccount) - .where(whereClause); + const [rows, [totalResult]] = await Promise.all([ + db + .select({ + ircAccount, + user: { + id: user.id, + email: user.email, + name: user.name, + }, + }) + .from(ircAccount) + .leftJoin(user, eq(ircAccount.userId, user.id)) + .where(whereClause) + .orderBy(desc(ircAccount.createdAt)) + .limit(limit) + .offset(offset), + db.select({ count: count() }).from(ircAccount).where(whereClause), + ]); const total = Number(totalResult?.count ?? 0); diff --git a/src/app/api/admin/mailcow-accounts/route.ts b/src/app/api/admin/mailcow-accounts/route.ts index 8ea4a0d..c52fe3c 100644 --- a/src/app/api/admin/mailcow-accounts/route.ts +++ b/src/app/api/admin/mailcow-accounts/route.ts @@ -36,26 +36,24 @@ export async function GET(request: NextRequest) { } const whereClause = conditions.length > 0 ? and(...conditions) : undefined; - const rows = await db - .select({ - mailcowAccount, - user: { - id: user.id, - email: user.email, - name: user.name, - }, - }) - .from(mailcowAccount) - .leftJoin(user, eq(mailcowAccount.userId, user.id)) - .where(whereClause) - .orderBy(desc(mailcowAccount.createdAt)) - .limit(limit) - .offset(offset); - - const [totalResult] = await db - .select({ count: count() }) - .from(mailcowAccount) - .where(whereClause); + const [rows, [totalResult]] = await Promise.all([ + db + .select({ + mailcowAccount, + user: { + id: user.id, + email: user.email, + name: user.name, + }, + }) + .from(mailcowAccount) + .leftJoin(user, eq(mailcowAccount.userId, user.id)) + .where(whereClause) + .orderBy(desc(mailcowAccount.createdAt)) + .limit(limit) + .offset(offset), + db.select({ count: count() }).from(mailcowAccount).where(whereClause), + ]); const total = Number(totalResult?.count ?? 0); diff --git a/src/app/api/admin/stats/route.ts b/src/app/api/admin/stats/route.ts index 88b104b..7d1e8ca 100644 --- a/src/app/api/admin/stats/route.ts +++ b/src/app/api/admin/stats/route.ts @@ -13,39 +13,35 @@ export async function GET(request: NextRequest) { try { await requireAdminOrStaff(request); - // Get user stats - const [userStats] = await db - .select({ - total: count(user.id), - admins: sql`COUNT(*) FILTER (WHERE ${user.role} = 'admin')`, - staff: sql`COUNT(*) FILTER (WHERE ${user.role} = 'staff')`, - banned: sql`COUNT(*) FILTER (WHERE ${user.banned} = true)`, - }) - .from(user); - - // Get active sessions count - const [sessionStats] = await db - .select({ - total: count(session.id), - active: sql`COUNT(*) FILTER (WHERE ${session.expiresAt} > NOW())`, - }) - .from(session); - - // Get API keys count - const [apiKeyStats] = await db - .select({ - total: count(apikey.id), - enabled: sql`COUNT(*) FILTER (WHERE ${apikey.enabled} = true)`, - }) - .from(apikey); - - // Get OAuth clients count - const [oauthClientStats] = await db - .select({ - total: count(oauthClient.id), - disabled: sql`COUNT(*) FILTER (WHERE ${oauthClient.disabled} = true)`, - }) - .from(oauthClient); + const [[userStats], [sessionStats], [apiKeyStats], [oauthClientStats]] = + await Promise.all([ + db + .select({ + total: count(user.id), + admins: sql`COUNT(*) FILTER (WHERE ${user.role} = 'admin')`, + staff: sql`COUNT(*) FILTER (WHERE ${user.role} = 'staff')`, + banned: sql`COUNT(*) FILTER (WHERE ${user.banned} = true)`, + }) + .from(user), + db + .select({ + total: count(session.id), + active: sql`COUNT(*) FILTER (WHERE ${session.expiresAt} > NOW())`, + }) + .from(session), + db + .select({ + total: count(apikey.id), + enabled: sql`COUNT(*) FILTER (WHERE ${apikey.enabled} = true)`, + }) + .from(apikey), + db + .select({ + total: count(oauthClient.id), + disabled: sql`COUNT(*) FILTER (WHERE ${oauthClient.disabled} = true)`, + }) + .from(oauthClient), + ]); return Response.json({ users: { diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts index 36099a3..eba2dc7 100644 --- a/src/app/api/admin/users/route.ts +++ b/src/app/api/admin/users/route.ts @@ -37,18 +37,19 @@ export async function GET(request: NextRequest) { const whereClause = conditions.length > 0 ? and(...conditions) : undefined; - const users = await db - .select() - .from(user) - .where(whereClause) - .orderBy(desc(user.createdAt)) - .limit(limit) - .offset(offset); - - const [{ count }] = await db - .select({ count: db.$count(user, whereClause) }) - .from(user) - .limit(1); + const [users, [{ count }]] = await Promise.all([ + db + .select() + .from(user) + .where(whereClause) + .orderBy(desc(user.createdAt)) + .limit(limit) + .offset(offset), + db + .select({ count: db.$count(user, whereClause) }) + .from(user) + .limit(1), + ]); return Response.json({ users, diff --git a/src/app/api/admin/xmpp-accounts/route.ts b/src/app/api/admin/xmpp-accounts/route.ts index 3dba3fa..3f9ac59 100644 --- a/src/app/api/admin/xmpp-accounts/route.ts +++ b/src/app/api/admin/xmpp-accounts/route.ts @@ -38,26 +38,24 @@ export async function GET(request: NextRequest) { } const whereClause = conditions.length > 0 ? and(...conditions) : undefined; - const rows = await db - .select({ - xmppAccount, - user: { - id: user.id, - email: user.email, - name: user.name, - }, - }) - .from(xmppAccount) - .leftJoin(user, eq(xmppAccount.userId, user.id)) - .where(whereClause) - .orderBy(desc(xmppAccount.createdAt)) - .limit(limit) - .offset(offset); - - const [totalResult] = await db - .select({ count: count() }) - .from(xmppAccount) - .where(whereClause); + const [rows, [totalResult]] = await Promise.all([ + db + .select({ + xmppAccount, + user: { + id: user.id, + email: user.email, + name: user.name, + }, + }) + .from(xmppAccount) + .leftJoin(user, eq(xmppAccount.userId, user.id)) + .where(whereClause) + .orderBy(desc(xmppAccount.createdAt)) + .limit(limit) + .offset(offset), + db.select({ count: count() }).from(xmppAccount).where(whereClause), + ]); const total = Number(totalResult?.count ?? 0); diff --git a/src/features/routing/lib/permissions.ts b/src/features/routing/lib/permissions.ts index 8b49725..2e2a2e5 100644 --- a/src/features/routing/lib/permissions.ts +++ b/src/features/routing/lib/permissions.ts @@ -73,11 +73,11 @@ export function getNavigationItems( // Group routes by navigation group return resolvedConfig.navigationGroups - .sort((a, b) => a.order - b.order) + .toSorted((a, b) => a.order - b.order) .map((group) => { const items = accessibleRoutes .filter((route) => route.navigation?.group === group.id) - .sort( + .toSorted( (a, b) => (a.navigation?.order ?? 0) - (b.navigation?.order ?? 0) ); From 97296db87ef1b3f795d9268763a4db4c23da9cac Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 27 Feb 2026 03:21:05 +0000 Subject: [PATCH 3/6] fix(xmpp): switch Prosody REST client from Basic to Bearer token auth Prosody 13's mod_http_admin_api depends on mod_tokenauth and only accepts Bearer token authentication. Portal was sending HTTP Basic auth (username + password), which always returned 401 Unauthorized. Changes: - Add PROSODY_REST_TOKEN env var for Bearer token auth - Update createAuthHeader() to send Bearer instead of Basic - Update validateXmppConfig/isXmppConfigured to check for token - Keep legacy PROSODY_REST_USERNAME/PASSWORD vars for backward compat Discovered by testing Portal against atl.chat's Prosody 13.0.4 instance. --- src/features/integrations/lib/xmpp/client.ts | 13 +++++++++---- src/features/integrations/lib/xmpp/config.ts | 19 ++++++++++--------- src/features/integrations/lib/xmpp/keys.ts | 8 ++++++++ 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/features/integrations/lib/xmpp/client.ts b/src/features/integrations/lib/xmpp/client.ts index c0dc03d..c6823c6 100644 --- a/src/features/integrations/lib/xmpp/client.ts +++ b/src/features/integrations/lib/xmpp/client.ts @@ -27,12 +27,17 @@ export class ProsodyAccountNotFoundError extends Error { // Body: { "password": "..." } /** - * Create Basic Auth header for Prosody REST API + * Create Authorization header for Prosody REST API. + * mod_http_admin_api (Prosody 13+) requires Bearer token auth via mod_tokenauth. */ function createAuthHeader(): string { - const { username, password } = xmppConfig.prosody; - const credentials = Buffer.from(`${username}:${password}`).toString("base64"); - return `Basic ${credentials}`; + const { token } = xmppConfig.prosody; + if (!token) { + throw new Error( + "PROSODY_REST_TOKEN is required for mod_http_admin_api Bearer auth" + ); + } + return `Bearer ${token}`; } /** diff --git a/src/features/integrations/lib/xmpp/config.ts b/src/features/integrations/lib/xmpp/config.ts index fd935fe..9b096ac 100644 --- a/src/features/integrations/lib/xmpp/config.ts +++ b/src/features/integrations/lib/xmpp/config.ts @@ -22,10 +22,13 @@ export const xmppConfig = { // Use internal Docker network URL for same-network communication restUrl: env.PROSODY_REST_URL, - // Username for REST API authentication (admin JID, e.g. admin@atl.chat) - username: env.PROSODY_REST_USERNAME || "admin@atl.chat", + // Bearer token for mod_http_admin_api (mod_tokenauth in Prosody 13+) + // Generate via prosodyctl or OAuth2 client credentials grant + token: env.PROSODY_REST_TOKEN, - // Password for REST API authentication + // Legacy: username/password kept for backward-compat but unused by + // mod_http_admin_api in Prosody 13+ (it requires Bearer tokens) + username: env.PROSODY_REST_USERNAME || "admin@atl.chat", password: env.PROSODY_REST_PASSWORD, }, } as const; @@ -36,17 +39,16 @@ export const xmppConfig = { * This prevents blocking the entire application if XMPP is not configured */ export function validateXmppConfig(): void { - if (!xmppConfig.prosody.password) { + if (!xmppConfig.prosody.token) { const error = new Error( - "PROSODY_REST_PASSWORD environment variable is required" + "PROSODY_REST_TOKEN environment variable is required (Bearer token for mod_http_admin_api)" ); - // Capture to Sentry before throwing (if Sentry is initialized) try { captureException(error, { tags: { type: "configuration_error", module: "xmpp_config", - missing_var: "PROSODY_REST_PASSWORD", + missing_var: "PROSODY_REST_TOKEN", }, level: "error", }); @@ -60,7 +62,6 @@ export function validateXmppConfig(): void { const error = new Error( "PROSODY_REST_URL environment variable is required" ); - // Capture to Sentry before throwing (if Sentry is initialized) try { captureException(error, { tags: { @@ -81,5 +82,5 @@ export function validateXmppConfig(): void { * Check if XMPP is configured (non-throwing) */ export function isXmppConfigured(): boolean { - return !!(xmppConfig.prosody.password && xmppConfig.prosody.restUrl); + return !!(xmppConfig.prosody.token && xmppConfig.prosody.restUrl); } diff --git a/src/features/integrations/lib/xmpp/keys.ts b/src/features/integrations/lib/xmpp/keys.ts index 8be2ff2..73895b5 100644 --- a/src/features/integrations/lib/xmpp/keys.ts +++ b/src/features/integrations/lib/xmpp/keys.ts @@ -4,6 +4,12 @@ import { createEnv } from "@t3-oss/env-nextjs"; /** * Get validated XMPP environment variables * Uses t3-env for runtime validation and type safety + * + * Auth: Prosody's mod_http_admin_api requires Bearer token auth (mod_tokenauth). + * Set PROSODY_REST_TOKEN to a token generated via prosodyctl or OAuth2. + * Legacy PROSODY_REST_USERNAME/PASSWORD are kept for backward-compat but unused + * by mod_http_admin_api in Prosody 13+. + * * @returns Validated environment configuration for XMPP/Prosody integration */ export const keys = () => @@ -11,12 +17,14 @@ export const keys = () => server: { XMPP_DOMAIN: z.string().optional(), PROSODY_REST_URL: z.url().optional(), + PROSODY_REST_TOKEN: z.string().optional(), PROSODY_REST_USERNAME: z.string().optional(), PROSODY_REST_PASSWORD: z.string().optional(), }, runtimeEnv: { XMPP_DOMAIN: process.env.XMPP_DOMAIN, PROSODY_REST_URL: process.env.PROSODY_REST_URL, + PROSODY_REST_TOKEN: process.env.PROSODY_REST_TOKEN, PROSODY_REST_USERNAME: process.env.PROSODY_REST_USERNAME, PROSODY_REST_PASSWORD: process.env.PROSODY_REST_PASSWORD, }, From b12ca66f634659f3d0e63166515beb62295f2491 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 27 Feb 2026 03:34:05 +0000 Subject: [PATCH 4/6] fix(irc): fix Atheme JSON-RPC id type and empty param handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs prevented Portal from communicating with Atheme JSONRPC: 1. **id must be a string**: Atheme's jsonrpclib.c validates MOWGLI_JSON_TAG(id) == MOWGLI_JSON_TAG_STRING. Portal sent numeric id (1), causing Atheme to silently drop the request without sending any HTTP response (connection hangs until timeout). Fixed: id: 1 → id: "1" 2. **Empty params rejected**: Atheme's jsonrpcmethod_command checks if (*param == 0) and returns fault_badparams. Portal sent "" as the accountname for unauthenticated commands. Fixed: "" → "." (consistent with the cookie placeholder). Verified end-to-end against live Atheme 7.3.0-rc2: - NickServ REGISTER: ✅ "portaluser is now registered" - atheme.login: ✅ returns authcookie - atheme.ison: ✅ returns online status --- .../integrations/lib/irc/atheme/client.ts | 19 ++++++++++--------- .../integrations/irc/atheme/client.test.ts | 10 +++++----- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/features/integrations/lib/irc/atheme/client.ts b/src/features/integrations/lib/irc/atheme/client.ts index 3a928e3..e7408b5 100644 --- a/src/features/integrations/lib/irc/atheme/client.ts +++ b/src/features/integrations/lib/irc/atheme/client.ts @@ -3,33 +3,33 @@ import "server-only"; import { ircConfig } from "../config"; import type { AnyAthemeFaultCode, AthemeFault } from "../types"; -/** JSON-RPC 2.0 request */ +/** JSON-RPC 2.0 request — Atheme requires `id` as a string (not number) */ interface JsonRpcRequest { jsonrpc: "2.0"; method: string; params: string[]; - id: number; + id: string; } /** JSON-RPC 2.0 success response — result is always a string for atheme.command */ interface JsonRpcSuccess { jsonrpc: "2.0"; result: string; - id: number; + id: string; } /** JSON-RPC 2.0 success response with object result (atheme.ison) */ interface JsonRpcObjectSuccess { jsonrpc: "2.0"; result: T; - id: number; + id: string; } /** JSON-RPC 2.0 error response (Atheme fault) */ interface JsonRpcError { jsonrpc: "2.0"; error: { code: number; message: string }; - id: number; + id: string; } /** @@ -60,7 +60,7 @@ async function athemeRpc( jsonrpc: "2.0", method, params, - id: 1, + id: "1", }; const controller = new AbortController(); @@ -113,7 +113,8 @@ async function athemeRpc( } /** - * Call atheme.command (unauthenticated — cookie ".", account ""). + * Call atheme.command (unauthenticated — cookie ".", account "."). + * Atheme's JSONRPC rejects empty-string params, so use "." as placeholder. * Params: authcookie, account, sourceip, service, command, ...commandParams */ function athemeCommand(params: string[]): Promise { @@ -136,7 +137,7 @@ export async function registerNick( ): Promise { await athemeCommand([ ".", - "", + ".", sourceIp, "NickServ", "REGISTER", @@ -158,7 +159,7 @@ export async function dropNick( ): Promise { await athemeCommand([ ".", - "", + ".", sourceIp, "NickServ", "DROP", diff --git a/tests/lib/integrations/irc/atheme/client.test.ts b/tests/lib/integrations/irc/atheme/client.test.ts index e0455ab..675d24f 100644 --- a/tests/lib/integrations/irc/atheme/client.test.ts +++ b/tests/lib/integrations/irc/atheme/client.test.ts @@ -49,7 +49,7 @@ describe("Atheme Client", () => { const ip = "1.2.3.4"; mockFetch.mockResolvedValueOnce({ ok: true, - json: async () => ({ jsonrpc: "2.0", result: "Success", id: 1 }), + json: async () => ({ jsonrpc: "2.0", result: "Success", id: "1" }), }); // Act @@ -65,7 +65,7 @@ describe("Atheme Client", () => { method: "atheme.command", params: [ ".", - "", + ".", ip, "NickServ", "REGISTER", @@ -73,7 +73,7 @@ describe("Atheme Client", () => { password, email, ], - id: 1, + id: "1", }), }) ); @@ -90,7 +90,7 @@ describe("Atheme Client", () => { json: async () => ({ jsonrpc: "2.0", error: { code: 8, message: "Nick already registered" }, - id: 1, + id: "1", }), }); @@ -128,7 +128,7 @@ describe("Atheme Client", () => { try { mockFetch.mockResolvedValueOnce({ ok: true, - json: async () => ({ jsonrpc: "2.0", result: "Success", id: 1 }), + json: async () => ({ jsonrpc: "2.0", result: "Success", id: "1" }), }); // Act From def1135c59270b461272c17fa2806bb8d04aefc0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 27 Feb 2026 04:22:03 +0000 Subject: [PATCH 5/6] chore: add atl.chat patch for mod_http_oauth2 Patch to apply to the atl.chat repo (cursor/development-environment-setup-635d branch). Adds mod_http_oauth2 to Prosody for Bearer token generation needed by Portal. Apply with: cd atl.chat git checkout cursor/development-environment-setup-635d git am < patches/atl-chat-mod-http-oauth2.patch --- patches/atl-chat-mod-http-oauth2.patch | 104 +++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 patches/atl-chat-mod-http-oauth2.patch diff --git a/patches/atl-chat-mod-http-oauth2.patch b/patches/atl-chat-mod-http-oauth2.patch new file mode 100644 index 0000000..88bbd61 --- /dev/null +++ b/patches/atl-chat-mod-http-oauth2.patch @@ -0,0 +1,104 @@ +From dde222c33c9d17450aece2d2bb86d6ccb339c3cd Mon Sep 17 00:00:00 2001 +From: Cursor Agent +Date: Fri, 27 Feb 2026 03:49:49 +0000 +Subject: [PATCH] feat(xmpp): add mod_http_oauth2 for Bearer token auth + +Portal's mod_http_admin_api requires Bearer tokens (mod_tokenauth). +This adds mod_http_oauth2 to enable OAuth2 token generation: + +- Added mod_http_oauth2 to modules.list (Prosody community module) +- Enabled 'http_oauth2' and 'invites' in global modules_enabled +- Configured OAuth2: password grant, 24h access tokens, 30d refresh +- Added oauth2_registration_key for dynamic client registration + +Token generation flow for Portal: + 1. Register client: POST /oauth2/register + 2. Get token: POST /oauth2/token (grant_type=password, scope=prosody:operator) + 3. Use token: Authorization: Bearer + +Verified end-to-end: create user (200), get user, delete user (200). +--- + apps/prosody/config/prosody.cfg.lua | 28 +++++++++++++++++++++++++--- + apps/prosody/modules.list | 1 + + 2 files changed, 26 insertions(+), 3 deletions(-) + +diff --git a/apps/prosody/config/prosody.cfg.lua b/apps/prosody/config/prosody.cfg.lua +index 280c52d..97cdfd4 100644 +--- a/apps/prosody/config/prosody.cfg.lua ++++ b/apps/prosody/config/prosody.cfg.lua +@@ -64,6 +64,7 @@ modules_enabled = { + -- SECURITY & PRIVACY + -- =============================================== + "tokenauth", -- Token management for OAuth2 and other modules (prosody.im/doc/modules/mod_tokenauth) ++ "http_oauth2", -- OAuth2/OIDC Authorization Server (generates Bearer tokens for mod_http_admin_api) + "blocklist", -- User blocking functionality (XEP-0191) + "anti_spam", -- Spam prevention and detection + "spam_reporting", -- Spam reporting mechanisms (XEP-0377) +@@ -73,7 +74,7 @@ modules_enabled = { + -- REGISTRATION & USER MANAGEMENT + -- =============================================== + -- "register", -- Password changes (XEP-0077); registration disabled (Portal provisions) +- -- "invites", -- User invitation system ++ "invites", -- User invitation system (required by mod_http_admin_api and mod_http_oauth2) + "welcome", -- Welcome messages for new users + "support_contact", -- Add support JID to roster of newly registered users (in-band reg only; modules.prosody.im/mod_support_contact) + -- "watchregistrations", -- Disabled: no in-band registration (Portal provisions via mod_http_admin_api) +@@ -562,6 +563,26 @@ tls_channel_binding = Lua.os.getenv("PROSODY_TLS_CHANNEL_BINDING") ~= "false" + push_notification_with_body = Lua.os.getenv("PROSODY_PUSH_NOTIFICATION_WITH_BODY") == "true" + push_notification_with_sender = Lua.os.getenv("PROSODY_PUSH_NOTIFICATION_WITH_SENDER") == "true" + ++-- =============================================== ++-- OAUTH2 CONFIGURATION (mod_http_oauth2) ++-- =============================================== ++-- Enables Bearer token generation for Portal's mod_http_admin_api integration. ++-- Portal uses the Resource Owner Password Grant to obtain tokens. ++-- Tokens are also usable for OAUTHBEARER SASL auth. ++allowed_oauth2_grant_types = { ++ "authorization_code", ++ "device_code", ++ "password", -- Resource Owner Password Grant (Portal provisioning) ++} ++allowed_oauth2_response_types = { ++ "code", ++} ++oauth2_access_token_ttl = 86400 -- 24 hours ++oauth2_refresh_token_ttl = 2592000 -- 30 days ++oauth2_require_code_challenge = false -- Portal uses password grant, not PKCE ++-- Dynamic client registration (enables Portal to register as OAuth2 client) ++oauth2_registration_key = Lua.os.getenv("PROSODY_OAUTH2_REGISTRATION_KEY") or "dev-oauth2-registration-key" ++ + -- =============================================== + -- AUTHENTICATION & ACCOUNT POLICY + -- =============================================== +@@ -892,9 +913,9 @@ ssl = { + } + name = "pubsub." .. domain + modules_enabled = { "pubsub_feeds" } +--- Node "feed" pulls from allthingslinux.org; subscribe to feed@pubsub.domain ++-- Node "feed" pulls from [REDACTED].org; subscribe to feed@pubsub.domain + feeds = { +- feed = Lua.os.getenv("PROSODY_FEED_URL") or "https://allthingslinux.org/feed", ++ feed = Lua.os.getenv("PROSODY_FEED_URL") or "https://[REDACTED].org/feed", + } + add_permissions = { + ["prosody:registered"] = { "pubsub:create-node" }, +@@ -947,3 +968,4 @@ account_cleanup = { + inactive_period = Lua.tonumber(Lua.os.getenv("PROSODY_ACCOUNT_INACTIVE_PERIOD")) or (365 * 24 * 3600), + grace_period = Lua.tonumber(Lua.os.getenv("PROSODY_ACCOUNT_GRACE_PERIOD")) or (30 * 24 * 3600), + } ++ +diff --git a/apps/prosody/modules.list b/apps/prosody/modules.list +index 8610c83..0772eb9 100644 +--- a/apps/prosody/modules.list ++++ b/apps/prosody/modules.list +@@ -34,6 +34,7 @@ mod_pubsub_subscription + mod_pubsub_feeds + mod_groups_internal + mod_http_admin_api ++mod_http_oauth2 + mod_support_contact + mod_idlecompat + mod_http_pep_avatar +-- +2.43.0 + From 17023eaf865c28664b8a9d2be18b92446af707bc Mon Sep 17 00:00:00 2001 From: Logan Honeycutt Date: Mon, 2 Mar 2026 00:32:26 -0500 Subject: [PATCH 6/6] revert(patch): remove atl-chat-mod-http-oauth2.patch to rollback OAuth2 integration The patch for adding OAuth2 support via mod_http_oauth2 is removed. This reverts the previous addition of OAuth2 token generation for Bearer token authentication in the Portal's mod_http_admin_api. The removal is necessary due to unforeseen issues or changes in requirements that make the OAuth2 integration no longer needed or viable at this time. --- patches/atl-chat-mod-http-oauth2.patch | 104 ------------------------- 1 file changed, 104 deletions(-) delete mode 100644 patches/atl-chat-mod-http-oauth2.patch diff --git a/patches/atl-chat-mod-http-oauth2.patch b/patches/atl-chat-mod-http-oauth2.patch deleted file mode 100644 index 88bbd61..0000000 --- a/patches/atl-chat-mod-http-oauth2.patch +++ /dev/null @@ -1,104 +0,0 @@ -From dde222c33c9d17450aece2d2bb86d6ccb339c3cd Mon Sep 17 00:00:00 2001 -From: Cursor Agent -Date: Fri, 27 Feb 2026 03:49:49 +0000 -Subject: [PATCH] feat(xmpp): add mod_http_oauth2 for Bearer token auth - -Portal's mod_http_admin_api requires Bearer tokens (mod_tokenauth). -This adds mod_http_oauth2 to enable OAuth2 token generation: - -- Added mod_http_oauth2 to modules.list (Prosody community module) -- Enabled 'http_oauth2' and 'invites' in global modules_enabled -- Configured OAuth2: password grant, 24h access tokens, 30d refresh -- Added oauth2_registration_key for dynamic client registration - -Token generation flow for Portal: - 1. Register client: POST /oauth2/register - 2. Get token: POST /oauth2/token (grant_type=password, scope=prosody:operator) - 3. Use token: Authorization: Bearer - -Verified end-to-end: create user (200), get user, delete user (200). ---- - apps/prosody/config/prosody.cfg.lua | 28 +++++++++++++++++++++++++--- - apps/prosody/modules.list | 1 + - 2 files changed, 26 insertions(+), 3 deletions(-) - -diff --git a/apps/prosody/config/prosody.cfg.lua b/apps/prosody/config/prosody.cfg.lua -index 280c52d..97cdfd4 100644 ---- a/apps/prosody/config/prosody.cfg.lua -+++ b/apps/prosody/config/prosody.cfg.lua -@@ -64,6 +64,7 @@ modules_enabled = { - -- SECURITY & PRIVACY - -- =============================================== - "tokenauth", -- Token management for OAuth2 and other modules (prosody.im/doc/modules/mod_tokenauth) -+ "http_oauth2", -- OAuth2/OIDC Authorization Server (generates Bearer tokens for mod_http_admin_api) - "blocklist", -- User blocking functionality (XEP-0191) - "anti_spam", -- Spam prevention and detection - "spam_reporting", -- Spam reporting mechanisms (XEP-0377) -@@ -73,7 +74,7 @@ modules_enabled = { - -- REGISTRATION & USER MANAGEMENT - -- =============================================== - -- "register", -- Password changes (XEP-0077); registration disabled (Portal provisions) -- -- "invites", -- User invitation system -+ "invites", -- User invitation system (required by mod_http_admin_api and mod_http_oauth2) - "welcome", -- Welcome messages for new users - "support_contact", -- Add support JID to roster of newly registered users (in-band reg only; modules.prosody.im/mod_support_contact) - -- "watchregistrations", -- Disabled: no in-band registration (Portal provisions via mod_http_admin_api) -@@ -562,6 +563,26 @@ tls_channel_binding = Lua.os.getenv("PROSODY_TLS_CHANNEL_BINDING") ~= "false" - push_notification_with_body = Lua.os.getenv("PROSODY_PUSH_NOTIFICATION_WITH_BODY") == "true" - push_notification_with_sender = Lua.os.getenv("PROSODY_PUSH_NOTIFICATION_WITH_SENDER") == "true" - -+-- =============================================== -+-- OAUTH2 CONFIGURATION (mod_http_oauth2) -+-- =============================================== -+-- Enables Bearer token generation for Portal's mod_http_admin_api integration. -+-- Portal uses the Resource Owner Password Grant to obtain tokens. -+-- Tokens are also usable for OAUTHBEARER SASL auth. -+allowed_oauth2_grant_types = { -+ "authorization_code", -+ "device_code", -+ "password", -- Resource Owner Password Grant (Portal provisioning) -+} -+allowed_oauth2_response_types = { -+ "code", -+} -+oauth2_access_token_ttl = 86400 -- 24 hours -+oauth2_refresh_token_ttl = 2592000 -- 30 days -+oauth2_require_code_challenge = false -- Portal uses password grant, not PKCE -+-- Dynamic client registration (enables Portal to register as OAuth2 client) -+oauth2_registration_key = Lua.os.getenv("PROSODY_OAUTH2_REGISTRATION_KEY") or "dev-oauth2-registration-key" -+ - -- =============================================== - -- AUTHENTICATION & ACCOUNT POLICY - -- =============================================== -@@ -892,9 +913,9 @@ ssl = { - } - name = "pubsub." .. domain - modules_enabled = { "pubsub_feeds" } ---- Node "feed" pulls from allthingslinux.org; subscribe to feed@pubsub.domain -+-- Node "feed" pulls from [REDACTED].org; subscribe to feed@pubsub.domain - feeds = { -- feed = Lua.os.getenv("PROSODY_FEED_URL") or "https://allthingslinux.org/feed", -+ feed = Lua.os.getenv("PROSODY_FEED_URL") or "https://[REDACTED].org/feed", - } - add_permissions = { - ["prosody:registered"] = { "pubsub:create-node" }, -@@ -947,3 +968,4 @@ account_cleanup = { - inactive_period = Lua.tonumber(Lua.os.getenv("PROSODY_ACCOUNT_INACTIVE_PERIOD")) or (365 * 24 * 3600), - grace_period = Lua.tonumber(Lua.os.getenv("PROSODY_ACCOUNT_GRACE_PERIOD")) or (30 * 24 * 3600), - } -+ -diff --git a/apps/prosody/modules.list b/apps/prosody/modules.list -index 8610c83..0772eb9 100644 ---- a/apps/prosody/modules.list -+++ b/apps/prosody/modules.list -@@ -34,6 +34,7 @@ mod_pubsub_subscription - mod_pubsub_feeds - mod_groups_internal - mod_http_admin_api -+mod_http_oauth2 - mod_support_contact - mod_idlecompat - mod_http_pep_avatar --- -2.43.0 -