Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions scripts/create-prosody-oauth-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,17 @@ import { oauthClient } from "@/lib/db/schema/oauth";
//
// Output:
// Prints the client_id and client_secret that should be set in Prosody
// configuration as PROSODY_OAUTH_CLIENT_ID and PROSODY_OAUTH_CLIENT_SECRET
/**
* Register a Prosody OAuth client in the application's database and print its credentials.
*
* Reads PROSODY_CLIENT_NAME (defaults to "Prosody XMPP Server") and optional PROSODY_CLIENT_ID
* from environment. If a client with the configured name already exists, logs its details and exits.
*
* Side effects: inserts a new oauth client record into the database and prints the client ID/secret
* to stdout for use in Prosody configuration.
*
* @throws Error if creating the OAuth client fails or no client record is returned after insertion.
*/

async function createProsodyOAuthClient() {
const clientName = process.env.PROSODY_CLIENT_NAME || "Prosody XMPP Server";
Expand Down Expand Up @@ -109,4 +119,4 @@ if (require.main === module) {
});
}

export { createProsodyOAuthClient };
export { createProsodyOAuthClient };
16 changes: 13 additions & 3 deletions src/app/(dashboard)/app/xmpp/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ import { verifySession } from "@/lib/auth/dal";
import { getServerRouteResolver, routeConfig } from "@/lib/routes";
import { getRouteMetadata } from "@/lib/seo";

// Metadata is automatically generated from route config
/**
* Produce route metadata for the XMPP account management page.
*
* Uses a server route resolver together with the route configuration to build metadata for the "/app/xmpp" route.
*
* @returns Metadata for the "/app/xmpp" route
*/
export async function generateMetadata(): Promise<Metadata> {
const resolver = await getServerRouteResolver();
return getRouteMetadata("/app/xmpp", routeConfig, resolver);
Expand All @@ -18,7 +24,11 @@ export async function generateMetadata(): Promise<Metadata> {
// ============================================================================
// XMPP Account Management Page
// ============================================================================
// Page for managing XMPP accounts - create, view, update, and delete
/**
* Renders the XMPP account management page after verifying the user session and priming the server-side query cache for XMPP accounts, prepared for client hydration.
*
* @returns The server-rendered React element for the XMPP account management UI wrapped with hydration state.
*/

export default async function XmppPage() {
// Verify user session
Expand Down Expand Up @@ -50,4 +60,4 @@ export default async function XmppPage() {
</div>
</HydrationBoundary>
);
}
}
32 changes: 24 additions & 8 deletions src/app/api/xmpp/accounts/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ import { formatJid, isValidXmppUsername } from "@/lib/xmpp/utils";
export const dynamic = "force-dynamic";

/**
* GET /api/xmpp/accounts/[id]
* Get specific XMPP account details (admin or owner only)
* Return details for a specific XMPP account if the requester is the account owner or an admin.
*
* Responds with the account's id, jid, username, status, createdAt, updatedAt, and metadata on success.
*
* @param request - The incoming Next.js request
* @param params - Route parameters; must include `id` of the XMPP account
* @returns The HTTP JSON response. On success: `{ ok: true, account: { id, jid, username, status, createdAt, updatedAt, metadata } }`. On failure: `{ ok: false, error }` with status `404` when not found, `403` when access is forbidden, or an appropriate error status for other failures.
*/
export async function GET(
request: NextRequest,
Expand Down Expand Up @@ -63,9 +68,11 @@ export async function GET(
}

/**
* PATCH /api/xmpp/accounts/[id]
* Update XMPP account (username, status, metadata)
* Note: Changing username may require recreating the Prosody account
* Update an XMPP account's username, status, or metadata, allowing the account owner or an admin to make changes.
*
* Validates username format and uniqueness when changed; note that changing a username typically requires recreating the Prosody account.
*
* @returns The JSON response: on success `{ ok: true, account: { id, jid, username, status, createdAt, updatedAt, metadata } }`; on failure `{ ok: false, error }` with an appropriate HTTP status code.
*/
export async function PATCH(
request: NextRequest,
Expand Down Expand Up @@ -185,8 +192,17 @@ export async function PATCH(
}

/**
* DELETE /api/xmpp/accounts/[id]
* Delete/suspend XMPP account (soft delete)
* Soft-delete an XMPP account and remove its Prosody account when present.
*
* Attempts to remove the account from Prosody (non-fatal if the Prosody account is not found),
* then marks the account record's status as "deleted" in the database if the caller is the
* account owner or an admin.
*
* @returns A Response containing `{ ok: true, message: "XMPP account deleted successfully" }` on success,
* or `{ ok: false, error: string }` with an appropriate HTTP status (403 for forbidden, 404 for not found,
* 500 for internal errors) on failure.
* @throws APIError If deleting the Prosody account fails for reasons other than "not found", or if the
* database update to mark the account deleted fails.
*/
export async function DELETE(
request: NextRequest,
Expand Down Expand Up @@ -255,4 +271,4 @@ export async function DELETE(
} catch (error) {
return handleAPIError(error);
}
}
}
12 changes: 9 additions & 3 deletions src/app/api/xmpp/accounts/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,14 @@ export async function POST(request: NextRequest) {
}

/**
* GET /api/xmpp/accounts
* Get current user's XMPP account information
* Retrieve the authenticated user's XMPP account record.
*
* Returns the user's XMPP account details if one exists for the authenticated user,
* or an error response when no account is found.
*
* @returns A JSON Response:
* - Success (200): `{ ok: true, account: { id, jid, username, status, createdAt, updatedAt, metadata } }`
* - Not found (404): `{ ok: false, error: "XMPP account not found" }`
*/
export async function GET(request: NextRequest) {
try {
Expand Down Expand Up @@ -203,4 +209,4 @@ export async function GET(request: NextRequest) {
} catch (error) {
return handleAPIError(error);
}
}
}
7 changes: 6 additions & 1 deletion src/components/xmpp/xmpp-account-management.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ import {
} from "@/hooks/use-xmpp-account";
import type { XmppAccountStatus } from "@/lib/xmpp/types";

/**
* Render the XMPP account management UI allowing users to view, create, and delete their XMPP account.
*
* @returns A React element containing the XMPP account management interface, including loading and error states, a create-account form when no account exists, detailed account information when present (JID, username, status, creation date, connection instructions), copy-to-clipboard and toast feedback, and a confirmation dialog for destructive deletion.
*/
export function XmppAccountManagement() {
const { data: account, isLoading, error } = useXmppAccount();
const createMutation = useCreateXmppAccount();
Expand Down Expand Up @@ -289,4 +294,4 @@ export function XmppAccountManagement() {
</CardFooter>
</Card>
);
}
}
29 changes: 23 additions & 6 deletions src/hooks/use-xmpp-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import type {
// TanStack Query hooks for XMPP account management

/**
* Fetch current user's XMPP account
* Fetches the current user's XMPP account.
*
* @returns The React Query result containing the current user's XMPP account data
*/
export function useXmppAccount() {
return useQuery({
Expand All @@ -32,7 +34,12 @@ export function useXmppAccount() {
}

/**
* Fetch a specific XMPP account by ID
* Provides a React Query hook that fetches an XMPP account by its ID.
*
* The query will not run when `id` is falsy and considers fetched data fresh for 1 minute.
*
* @param id - The XMPP account identifier
* @returns The query result containing the XMPP account data; `data` is `undefined` while loading or when `id` is falsy.
*/
export function useXmppAccountById(id: string) {
return useQuery({
Expand All @@ -44,7 +51,9 @@ export function useXmppAccountById(id: string) {
}

/**
* Create a new XMPP account for the current user
* Creates a new XMPP account for the current user.
*
* @returns The mutation object used to create an XMPP account. Invoking the mutation sends the create request and, on success, invalidates the cached current XMPP account to trigger a refetch.
*/
export function useCreateXmppAccount() {
const queryClient = useQueryClient();
Expand All @@ -61,7 +70,13 @@ export function useCreateXmppAccount() {
}

/**
* Update an XMPP account
* Create a React Query mutation hook to update an XMPP account and keep related caches in sync.
*
* On successful mutation the hook sets the returned account data into the detail cache for the
* updated account id and invalidates the current XMPP account query to trigger a refetch.
*
* @returns A mutation result for performing XMPP account updates; the mutation function expects
* an object `{ id, data }` and resolves to the updated account data.
*/
export function useUpdateXmppAccount() {
const queryClient = useQueryClient();
Expand Down Expand Up @@ -89,7 +104,9 @@ export function useUpdateXmppAccount() {
}

/**
* Delete an XMPP account
* Provides a React Query mutation hook to delete an XMPP account and keep related caches in sync.
*
* @returns A mutation object whose mutate/mutateAsync function accepts an XMPP account `id` (string) and, on success, invalidates XMPP account queries so related data is refetched.
*/
export function useDeleteXmppAccount() {
const queryClient = useQueryClient();
Expand All @@ -103,4 +120,4 @@ export function useDeleteXmppAccount() {
});
},
});
}
}
31 changes: 25 additions & 6 deletions src/lib/api/xmpp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ interface XmppApiResponse<T> {
}

/**
* Fetch current user's XMPP account
* Retrieve the current user's XMPP account.
*
* @returns The user's `XmppAccount` if one exists, or `null` if the server reports no account (404) or the response contains no account.
* @throws Error when the HTTP response is not successful (except 404); the error message contains the server-provided error text or the response status text.
*/
export async function fetchXmppAccount(): Promise<XmppAccount | null> {
const response = await fetch("/api/xmpp/accounts");
Expand All @@ -42,7 +45,11 @@ export async function fetchXmppAccount(): Promise<XmppAccount | null> {
}

/**
* Fetch a specific XMPP account by ID
* Retrieve the XMPP account with the specified ID.
*
* @param id - The ID of the XMPP account to fetch
* @returns The XMPP account corresponding to `id`
* @throws When the server responds with a non-OK status or when the response does not include an account
*/
export async function fetchXmppAccountById(id: string): Promise<XmppAccount> {
const response = await fetch(`/api/xmpp/accounts/${id}`);
Expand All @@ -64,7 +71,11 @@ export async function fetchXmppAccountById(id: string): Promise<XmppAccount> {
}

/**
* Create a new XMPP account for the current user
* Creates a new XMPP account for the current user.
*
* @param data - Payload with the fields required to create the account
* @returns The created `XmppAccount`
* @throws Error if the API request fails or the response does not include an account
*/
export async function createXmppAccount(
data: CreateXmppAccountRequest
Expand Down Expand Up @@ -92,7 +103,12 @@ export async function createXmppAccount(
}

/**
* Update an XMPP account
* Update an existing XMPP account by ID.
*
* @param id - The ID of the XMPP account to update
* @param data - Partial account fields to update
* @returns The updated XMPP account
* @throws Error if the server responds with an error or if the response does not include the updated account
*/
export async function updateXmppAccount(
id: string,
Expand Down Expand Up @@ -121,7 +137,10 @@ export async function updateXmppAccount(
}

/**
* Delete an XMPP account
* Delete the XMPP account with the specified ID.
*
* @param id - The ID of the XMPP account to delete.
* @throws Error if the server responds with a non-OK status; the error message will include the server-provided message when available.
*/
export async function deleteXmppAccount(id: string): Promise<void> {
const response = await fetch(`/api/xmpp/accounts/${id}`, {
Expand All @@ -136,4 +155,4 @@ export async function deleteXmppAccount(id: string): Promise<void> {
error.error || `Failed to delete XMPP account: ${response.statusText}`
);
}
}
}
29 changes: 20 additions & 9 deletions src/lib/xmpp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import { formatJid } from "./utils";
// Documentation: https://modules.prosody.im/mod_rest.html

/**
* Create Basic Auth header for Prosody REST API
* Builds the HTTP Basic Authorization header for the Prosody REST API using configured credentials.
*
* @returns The Authorization header string in the form `Basic <base64(username:password)>`
*/
function createAuthHeader(): string {
const { username, password } = xmppConfig.prosody;
Expand All @@ -22,7 +24,12 @@ function createAuthHeader(): string {
}

/**
* Make a request to Prosody REST API
* Send an HTTP request to the Prosody mod_rest API and return the parsed response.
*
* @param endpoint - The API path to append to the configured Prosody `restUrl` (for example `/accounts`).
* @param options - Fetch options (method, headers, body, etc.). Provided headers are merged with the required `Content-Type: application/xml` and `Authorization` header.
* @returns The response parsed as JSON when the `Content-Type` includes `application/json`, otherwise the response body text cast to `T`.
* @throws Error when the response status is not ok; the thrown message is taken from the response JSON `error` or `message` fields when present, otherwise includes HTTP status and statusText.
*/
async function prosodyRequest<T>(
endpoint: string,
Expand Down Expand Up @@ -63,11 +70,13 @@ async function prosodyRequest<T>(
}

/**
* Create XMPP account in Prosody
* Note: No password is set - authentication is handled via OAuth
* Create an XMPP account in Prosody.
*
* Does not set a password; authentication is handled via OAuth.
*
* @param username - XMPP localpart (username)
* @returns Success response
* @returns The Prosody REST API response for the account creation request
* @throws Error with message `XMPP account already exists: <jid>` if the account already exists; rethrows other errors
*/
export async function createProsodyAccount(
username: string
Expand Down Expand Up @@ -114,10 +123,12 @@ export async function createProsodyAccount(
}

/**
* Delete XMPP account from Prosody
* Delete an XMPP account from the Prosody server.
*
* @param username - XMPP localpart (username)
* @returns Success response
* If the account does not exist, the function treats the outcome as successful (idempotent).
*
* @param username - The XMPP account localpart to delete
* @returns The Prosody REST API response; returns `{ success: true }` if the account was deleted or did not exist
*/
export async function deleteProsodyAccount(
username: string
Expand Down Expand Up @@ -184,4 +195,4 @@ export async function checkProsodyAccountExists(
// Re-throw other errors
throw error;
}
}
}
Loading