Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
251 lines
6.9 KiB
Markdown
251 lines
6.9 KiB
Markdown
# User Host API Guide
|
|
|
|
This guide explains how to use the `cap:user.read` capability to access current user information in your extension.
|
|
|
|
## Overview
|
|
|
|
The User Host API allows extensions to retrieve information about the currently authenticated user making the request. This is useful for:
|
|
|
|
- **Personalization**: Customize responses based on the user's identity
|
|
- **Audit logging**: Track which user triggered an action
|
|
- **Authorization**: Make decisions based on user type (MSP vs client)
|
|
- **Multi-tenant awareness**: Access tenant and client context
|
|
|
|
## Prerequisites
|
|
|
|
The `cap:user.read` capability is included in the default capabilities for all extensions, so you don't need to explicitly declare it. However, if you're customizing capabilities, ensure it's included:
|
|
|
|
```json
|
|
{
|
|
"capabilities": ["cap:user.read", "cap:log.emit"]
|
|
}
|
|
```
|
|
|
|
## API Reference
|
|
|
|
### UserHost Interface
|
|
|
|
```typescript
|
|
interface UserHost {
|
|
getUser(): Promise<UserData>;
|
|
}
|
|
```
|
|
|
|
### Types
|
|
|
|
```typescript
|
|
interface UserData {
|
|
/** Tenant ID the user belongs to */
|
|
tenantId: string;
|
|
/** Client/company name */
|
|
clientName: string;
|
|
/** Unique user identifier */
|
|
userId: string;
|
|
/** User's email address */
|
|
userEmail: string;
|
|
/** User's display name */
|
|
userName: string;
|
|
/** User type: "internal" (MSP staff) or "client" (client portal user) */
|
|
userType: string;
|
|
/** For client portal users, the client_id they are associated with */
|
|
clientId?: string;
|
|
/** Optional additional attributes provided by the host */
|
|
additionalFields?: Record<string, string>;
|
|
}
|
|
|
|
type UserError = 'not-available' | 'not-allowed';
|
|
```
|
|
|
|
## Usage Examples
|
|
|
|
### Basic Usage
|
|
|
|
```typescript
|
|
try {
|
|
const user = await host.user.getUser();
|
|
console.log(`Request from: ${user.userName} (${user.userEmail})`);
|
|
console.log(`User type: ${user.userType}`);
|
|
} catch (err) {
|
|
// User info not available (e.g., service-to-service call)
|
|
console.log('No user context available');
|
|
}
|
|
```
|
|
|
|
### Conditional Logic Based on User Type
|
|
|
|
```typescript
|
|
const user = await host.user.getUser();
|
|
|
|
if (user.userType === 'internal') {
|
|
// MSP staff - show full data
|
|
return jsonResponse({
|
|
data: allRecords,
|
|
canEdit: true
|
|
});
|
|
} else {
|
|
// Client portal user - show filtered data
|
|
return jsonResponse({
|
|
data: filteredRecords,
|
|
canEdit: false
|
|
});
|
|
}
|
|
```
|
|
|
|
### Including User Info in Responses
|
|
|
|
```typescript
|
|
export async function handler(request: ExecuteRequest, host: HostBindings): Promise<ExecuteResponse> {
|
|
let user = null;
|
|
try {
|
|
user = await host.user.getUser();
|
|
} catch {
|
|
// Continue without user info
|
|
}
|
|
|
|
return jsonResponse({
|
|
ok: true,
|
|
message: 'Hello from extension',
|
|
user: user ? {
|
|
name: user.userName,
|
|
email: user.userEmail,
|
|
type: user.userType,
|
|
} : null,
|
|
});
|
|
}
|
|
```
|
|
|
|
## Setting Up Host Bindings
|
|
|
|
To use the User API (or any host capability), your extension needs a wrapper that imports the WIT functions. Create an `index.ts` that builds the `HostBindings` object:
|
|
|
|
```typescript
|
|
// src/index.ts
|
|
import { handler as userHandler } from './handler-impl.js';
|
|
import { ExecuteRequest, ExecuteResponse, HostBindings, normalizeUserData } from '@alga-psa/extension-runtime';
|
|
|
|
// Import WIT functions (these are resolved at runtime by jco)
|
|
// @ts-ignore
|
|
import { getUser } from 'alga:extension/user-v2';
|
|
// @ts-ignore
|
|
import { logInfo, logWarn, logError } from 'alga:extension/logging';
|
|
// @ts-ignore
|
|
import { getContext } from 'alga:extension/context';
|
|
// ... other imports as needed
|
|
|
|
// Build the HostBindings object
|
|
const host: HostBindings = {
|
|
context: {
|
|
get: async () => getContext(),
|
|
},
|
|
logging: {
|
|
info: async (msg: string) => logInfo(msg),
|
|
warn: async (msg: string) => logWarn(msg),
|
|
error: async (msg: string) => logError(msg),
|
|
},
|
|
user: {
|
|
getUser: async () => normalizeUserData(await getUser()),
|
|
},
|
|
// ... other bindings
|
|
};
|
|
|
|
// Export WIT-compatible handler
|
|
export async function handler(request: ExecuteRequest): Promise<ExecuteResponse> {
|
|
return userHandler(request, host);
|
|
}
|
|
```
|
|
|
|
## Build Configuration
|
|
|
|
When using WIT imports, your build must mark them as external. Add a custom build script to `package.json`:
|
|
|
|
```json
|
|
{
|
|
"scripts": {
|
|
"build:backend": "esbuild src/index.ts --bundle --format=esm --platform=neutral --outfile=dist/js/index.js --external:alga:extension/secrets --external:alga:extension/http --external:alga:extension/storage --external:alga:extension/logging --external:alga:extension/ui-proxy --external:alga:extension/context --external:alga:extension/user --external:alga:extension/user-v2",
|
|
"build:component": "jco componentize dist/js/index.js --wit ./wit/extension-runner.wit --world-name runner --disable all --out dist/main.wasm",
|
|
"build": "npm run build:backend && npm run build:component"
|
|
},
|
|
"devDependencies": {
|
|
"@bytecodealliance/jco": "^1.8.0",
|
|
"esbuild": "^0.20.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
## WIT Definition
|
|
|
|
Ensure your `wit/extension-runner.wit` includes the user interface (v2):
|
|
|
|
```wit
|
|
record user-data-v2 {
|
|
tenant-id: string,
|
|
client-name: string,
|
|
user-id: string,
|
|
user-email: string,
|
|
user-name: string,
|
|
user-type: string,
|
|
client-id: option<string>,
|
|
additional-fields: list<tuple<string, string>>,
|
|
}
|
|
|
|
enum user-error {
|
|
not-available,
|
|
not-allowed,
|
|
}
|
|
|
|
interface user-v2 {
|
|
get-user: func() -> result<user-data-v2, user-error>;
|
|
}
|
|
|
|
world runner {
|
|
// ... other imports
|
|
import user-v2;
|
|
|
|
export handler: func(request: execute-request) -> execute-response;
|
|
}
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
The `getUser()` function can throw errors in these cases:
|
|
|
|
| Error | Cause |
|
|
|-------|-------|
|
|
| `not-available` | No user session (e.g., service-to-service call, scheduled task) |
|
|
| `not-allowed` | The `cap:user.read` capability was not granted |
|
|
|
|
Always wrap `getUser()` in a try-catch:
|
|
|
|
```typescript
|
|
let userName = 'Anonymous';
|
|
try {
|
|
const user = await host.user.getUser();
|
|
userName = user.userName;
|
|
} catch (err) {
|
|
await host.logging.info('No user context available for this request');
|
|
}
|
|
```
|
|
|
|
## When User Info Is Not Available
|
|
|
|
User information is extracted from the session. It will be `null` or throw `not-available` in these scenarios:
|
|
|
|
- **Scheduled task invocations**: Tasks run by the scheduler don't have a user session
|
|
- **Service-to-service calls**: API calls using `x-alga-tenant` header instead of session
|
|
- **Webhook callbacks**: External systems calling your extension endpoints
|
|
|
|
Design your extension to handle both authenticated and unauthenticated contexts gracefully.
|
|
|
|
## Security Notes
|
|
|
|
- User data is read-only; extensions cannot modify user information
|
|
- The `cap:user.read` capability is granted by default but can be revoked
|
|
- User IDs are tenant-scoped and may differ across tenants
|
|
- Do not expose sensitive user information to client-side code
|
|
|
|
## See Also
|
|
|
|
- [Scheduler Host API Guide](./scheduler-host-api.md)
|
|
- [Sample Client Portal Extension](../../../ee/extensions/samples/client-portal-test/)
|
|
- [Extension Runtime Reference](../../extension-runtime/)
|