@elqnt/admin 2.2.1 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -14
- package/SKILL.md +570 -0
- package/dist/admin-C1iVQe2d.d.cts +816 -0
- package/dist/admin-C1iVQe2d.d.ts +816 -0
- package/dist/api/index.cjs +85 -281
- package/dist/api/index.cjs.map +1 -1
- package/dist/api/index.d.cts +17 -53
- package/dist/api/index.d.ts +17 -53
- package/dist/api/index.js +83 -257
- package/dist/api/index.js.map +1 -1
- package/dist/hooks/index.cjs +131 -841
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.cts +69 -188
- package/dist/hooks/index.d.ts +69 -188
- package/dist/hooks/index.js +131 -836
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +94 -473
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +89 -401
- package/dist/index.js.map +1 -1
- package/dist/models/index.cjs +9 -192
- package/dist/models/index.cjs.map +1 -1
- package/dist/models/index.d.cts +3 -1263
- package/dist/models/index.d.ts +3 -1263
- package/dist/models/index.js +6 -144
- package/dist/models/index.js.map +1 -1
- package/dist/orgs-BSHeYVe-.d.ts +53 -0
- package/dist/orgs-Buu2Re0N.d.cts +53 -0
- package/package.json +6 -5
- package/dist/provisioning-Cfl6wbmV.d.cts +0 -168
- package/dist/provisioning-Il9t2jnH.d.ts +0 -168
package/README.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Admin APIs and types for the Eloquent platform - onboarding, organization settings, billing, and user management.
|
|
4
4
|
|
|
5
|
+
> **Building an admin app / using an AI coding agent?** Read [`SKILL.md`](./SKILL.md)
|
|
6
|
+
> — the full agent guide: the one-way architecture (admin app → `@elqnt/admin`
|
|
7
|
+
> hooks → API Gateway → admin Go service), the gateway-token flow, and the exact
|
|
8
|
+
> spec of every hook (`useOrgAdmin` — org CRUD + settings; `useUsersAdmin` — user
|
|
9
|
+
> CRUD + invitations) and its methods. It ships in the package, so the contract is
|
|
10
|
+
> this package's own `.d.ts` — your code type-checks against it.
|
|
11
|
+
> (Product analytics moved to `@elqnt/analytics`.)
|
|
12
|
+
|
|
5
13
|
## Installation
|
|
6
14
|
|
|
7
15
|
```bash
|
|
@@ -33,7 +41,7 @@ const plans = await getBillingPlansApi({
|
|
|
33
41
|
| `@elqnt/admin` | All exports |
|
|
34
42
|
| `@elqnt/admin/api` | Browser API functions |
|
|
35
43
|
| `@elqnt/admin/models` | TypeScript types |
|
|
36
|
-
| `@elqnt/admin/hooks` | React hooks (`useOrgAdmin`, `useUsersAdmin
|
|
44
|
+
| `@elqnt/admin/hooks` | React hooks (`useOrgAdmin`, `useUsersAdmin`) |
|
|
37
45
|
|
|
38
46
|
## APIs
|
|
39
47
|
|
|
@@ -55,19 +63,21 @@ const plans = await getBillingPlansApi({
|
|
|
55
63
|
## Hooks
|
|
56
64
|
|
|
57
65
|
```typescript
|
|
58
|
-
import { useOrgAdmin, useUsersAdmin
|
|
59
|
-
|
|
60
|
-
// Organization management
|
|
61
|
-
const {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
import { useOrgAdmin, useUsersAdmin } from "@elqnt/admin/hooks";
|
|
67
|
+
|
|
68
|
+
// Organization management + settings
|
|
69
|
+
const {
|
|
70
|
+
listOrgs, getOrg, createOrg, updateOrg, deleteOrg,
|
|
71
|
+
getSettings, createSettings, updateSettings,
|
|
72
|
+
loading, error,
|
|
73
|
+
} = useOrgAdmin(options);
|
|
74
|
+
|
|
75
|
+
// User management + invitations
|
|
76
|
+
const {
|
|
77
|
+
listUsers, getUser, createUser, updateUser, deleteUser, getUserSettings, updateUserSettings,
|
|
78
|
+
listInvites, sendInvite, sendInvites, getInvite, acceptInvite, revokeInvite, resendInvite,
|
|
79
|
+
loading, error,
|
|
80
|
+
} = useUsersAdmin(options);
|
|
71
81
|
```
|
|
72
82
|
|
|
73
83
|
## Models
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: admin
|
|
3
|
+
description: Build an admin/platform frontend on the Eloquent admin backend using the @elqnt/admin hooks. Covers the one true path (admin app → @elqnt/admin hooks → API Gateway → admin Go service), the gateway-token/secret flow, and the EXACT input/output spec of every hook method (useOrgAdmin — org CRUD + settings; useUsersAdmin — user CRUD + invitations) plus the onboarding/billing api functions and all DTOs. Product analytics moved to @elqnt/analytics.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Admin — Building an Admin / Platform Frontend
|
|
7
|
+
|
|
8
|
+
Eloquent's **admin** service owns the platform's control plane: organizations,
|
|
9
|
+
users, invitations, org settings, onboarding, and billing. This skill is **only**
|
|
10
|
+
about one thing: building a frontend (the admin console, an onboarding wizard)
|
|
11
|
+
that drives it through the `@elqnt/admin` package.
|
|
12
|
+
|
|
13
|
+
> Package is `@elqnt/admin`. It exports **two React hooks**:
|
|
14
|
+
> - `useOrgAdmin` — organization CRUD **plus org settings** (the settings methods
|
|
15
|
+
> were merged in from the former `useOrgSettings`).
|
|
16
|
+
> - `useUsersAdmin` — user CRUD **plus invitations** (the invite methods were
|
|
17
|
+
> merged in from the former `useInvitesAdmin`).
|
|
18
|
+
>
|
|
19
|
+
> plus a set of **api functions** for the flows that have no hook yet (onboarding
|
|
20
|
+
> wizard, billing/Stripe). Hooks are the primary interface; api functions are the
|
|
21
|
+
> building blocks the hooks wrap.
|
|
22
|
+
>
|
|
23
|
+
> **Product analytics moved out** to its own package — `@elqnt/analytics`
|
|
24
|
+
> (`useProductAnalytics`). See that package's `SKILL.md`.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## ⛔ The contract — read this before writing any code
|
|
29
|
+
|
|
30
|
+
This skill ships **inside the package**, so the contract is not a separate file
|
|
31
|
+
to keep in sync — it **is** the package's own published type declarations
|
|
32
|
+
(`@elqnt/admin` → `dist/**/*.d.ts`). Because your app *imports the real hooks*,
|
|
33
|
+
the TypeScript compiler enforces the contract for you: a wrong param or a misused
|
|
34
|
+
return value won't compile.
|
|
35
|
+
|
|
36
|
+
The single source of truth, in order:
|
|
37
|
+
|
|
38
|
+
1. `import { useOrgAdmin, useUsersAdmin } from "@elqnt/admin/hooks"`
|
|
39
|
+
— the two hooks.
|
|
40
|
+
2. `import type { UseOrgAdminReturn, UseUsersAdminReturn } from "@elqnt/admin/hooks"`
|
|
41
|
+
— the **named, exact** method surface of each hook (every method, its params,
|
|
42
|
+
its return type). The tables below are a human-readable mirror of these.
|
|
43
|
+
3. `import type { … } from "@elqnt/admin/models"` — `Org`, `User`, `Invite`,
|
|
44
|
+
`OrgSettings`, etc. (the DTOs).
|
|
45
|
+
4. `import { … } from "@elqnt/admin/api"` — the onboarding/billing api functions
|
|
46
|
+
(and the lower-level CRUD api fns the hooks wrap) for flows no hook covers.
|
|
47
|
+
|
|
48
|
+
**Rules (do not drift):**
|
|
49
|
+
|
|
50
|
+
1. **Use only the two exported hooks** (and the documented api functions) and
|
|
51
|
+
only the methods/fns they expose. Do **not** invent hooks, methods, params, or
|
|
52
|
+
return shapes — if it's not on `UseOrgAdminReturn` / `UseUsersAdminReturn` /
|
|
53
|
+
the exported api signatures, it doesn't exist.
|
|
54
|
+
2. **Let the compiler check you.** Build with `tsc`; never `as any` / `@ts-ignore`
|
|
55
|
+
your way around a hook's types to force a call that isn't there.
|
|
56
|
+
3. **Never bypass the package.** No `fetch`/`axios` to `/api/v1/admin/...`, no
|
|
57
|
+
NATS, no direct service calls. (Server-side: mint a token with
|
|
58
|
+
`createServerClient` from `@elqnt/api-client/server` and call the same gateway
|
|
59
|
+
paths — same routes, same shapes.)
|
|
60
|
+
4. **Wrap, don't scatter.** Import the hook in exactly one app file
|
|
61
|
+
(`hooks/use-<domain>.ts` or a context provider); see ["The app layer"](#the-app-layer).
|
|
62
|
+
5. If a requirement seems to need a call the package doesn't define, **stop and
|
|
63
|
+
flag it** — do not improvise an endpoint.
|
|
64
|
+
|
|
65
|
+
The prose/tables below explain each method; the package's shipped `.d.ts` is what
|
|
66
|
+
your code type-checks against.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Architecture — the one and only path
|
|
71
|
+
|
|
72
|
+
A component **never** calls a package hook directly. It only sees your **domain
|
|
73
|
+
context** (orgs list, current user, invite table), served by a **context
|
|
74
|
+
provider**, backed by your **app hook**, which is the *only* place that wraps the
|
|
75
|
+
`@elqnt/admin` hook. Everything below the app hook is plumbing the component
|
|
76
|
+
never imports.
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
┌──────────────────────────────────────────────────────────────────────────────┐
|
|
80
|
+
│ YOUR ADMIN APP (Next.js / React) │
|
|
81
|
+
│ │
|
|
82
|
+
│ SSR layout: read API_GATEWAY_URL_PUBLIC (request-time) + ORG_ID │
|
|
83
|
+
│ └─► AppConfigProvider { apiGatewayUrl, orgId } │
|
|
84
|
+
│ │
|
|
85
|
+
│ components/orgs/* ── speak only the Org / User / Invite domain types │
|
|
86
|
+
│ │ useOrgsContext() │
|
|
87
|
+
│ ▼ │
|
|
88
|
+
│ contexts/orgs-context.tsx ── one shared app-hook instance │
|
|
89
|
+
│ │ │
|
|
90
|
+
│ ▼ │
|
|
91
|
+
│ hooks/use-orgs.ts ── app hook: CRUD + state │
|
|
92
|
+
│ │ │
|
|
93
|
+
│ ▼ │
|
|
94
|
+
│ useOrgAdmin / useUsersAdmin (@elqnt/admin/hooks) │
|
|
95
|
+
│ │ │
|
|
96
|
+
│ ▼ │
|
|
97
|
+
│ @elqnt/admin/api → browserApiRequest │
|
|
98
|
+
│ │ getGatewayToken() ⇒ GET /api/gateway-token (mint HS256 JWT, JWT_SECRET)│
|
|
99
|
+
│ │ attaches: Authorization: Bearer <jwt>, X-Org-ID, X-User-ID, X-Product│
|
|
100
|
+
└─────────┼──────────────────────────────────────────────────────────────────────┘
|
|
101
|
+
▼
|
|
102
|
+
┌─────────────────┐ verify JWT, stamp X-Org-ID/X-Product, route match
|
|
103
|
+
│ API GATEWAY │ /api/v1/admin/** , /api/v1/onboarding/** ,
|
|
104
|
+
└───────┬─────────┘ /api/v1/billing/** → admin svc
|
|
105
|
+
▼
|
|
106
|
+
┌─────────────────┐ per-product admin DB admin_<product> (row-level tenancy)
|
|
107
|
+
│ admin (Go) │ orgs / users / invites / settings / onboarding / billing
|
|
108
|
+
└─────────────────┘
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Rules: the frontend **never** calls the admin service directly and **never**
|
|
112
|
+
touches NATS. Every call is HTTP through the gateway carrying an org id + gateway
|
|
113
|
+
token. And **components never import `@elqnt/admin`** — only the app hook does.
|
|
114
|
+
|
|
115
|
+
> **Product routing.** There is no `product` column on the org row. The
|
|
116
|
+
> `product` you send (JWT claim server-side, `X-Product` header browser-side)
|
|
117
|
+
> selects which `admin_<product>` DB the request reads/writes (empty →
|
|
118
|
+
> `eloquent`). `listOrgs`/`createOrg` also accept a transient `product` field as
|
|
119
|
+
> routing metadata; it is **not** persisted on the org.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## The gateway token (the secret) — how the app gets it
|
|
124
|
+
|
|
125
|
+
Every request to the gateway needs `Authorization: Bearer <token>`. The token is
|
|
126
|
+
a short-lived **HS256 JWT** signed with the shared **gateway secret**. You never
|
|
127
|
+
hardcode it; there are two flows depending on where the code runs.
|
|
128
|
+
|
|
129
|
+
### Browser flow (what the hooks use)
|
|
130
|
+
|
|
131
|
+
The hooks → api fns → `browserApiRequest` → `getGatewayToken()` internally. You
|
|
132
|
+
do **not** pass a token to the hook. `getGatewayToken()` (from
|
|
133
|
+
`@elqnt/api-client/browser`) by default does:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
fetch("/api/gateway-token") ⇒ { token, expiresIn }
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
So your admin app must expose a **`/api/gateway-token` route** that mints the JWT
|
|
140
|
+
server-side (the secret stays on the server, never reaches the browser). The
|
|
141
|
+
client caches the token and refreshes ~5 min before expiry.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// app/api/gateway-token/route.ts (Next.js route handler — server only)
|
|
145
|
+
import { NextResponse } from "next/server";
|
|
146
|
+
import * as jose from "jose";
|
|
147
|
+
|
|
148
|
+
export async function GET() {
|
|
149
|
+
const secret = new TextEncoder().encode(process.env.JWT_SECRET!); // SAME secret the gateway validates with
|
|
150
|
+
const token = await new jose.SignJWT({
|
|
151
|
+
org_id: process.env.ORG_ID!,
|
|
152
|
+
user_id: "system",
|
|
153
|
+
email: "system@admin-app.com",
|
|
154
|
+
role: "admin", // admin routes typically require admin/super_admin
|
|
155
|
+
scopes: ["read", "write", "admin"], // OR-matched against the route's required scopes
|
|
156
|
+
product: "eloquent", // gateway resolves product from THIS claim first
|
|
157
|
+
})
|
|
158
|
+
.setProtectedHeader({ alg: "HS256" })
|
|
159
|
+
.setIssuedAt()
|
|
160
|
+
.setIssuer("eloquent-gateway") // must match gateway JWT_ISSUER
|
|
161
|
+
.setAudience("eloquent-api") // must match gateway JWT_AUDIENCE
|
|
162
|
+
.setExpirationTime("1h")
|
|
163
|
+
.sign(secret);
|
|
164
|
+
|
|
165
|
+
return NextResponse.json({ token, expiresIn: 3600 });
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
> The gateway re-stamps `X-Org-ID` / `X-User-ID` / `X-Product` from the **signed
|
|
170
|
+
> JWT claims** before proxying, so a forged header can't override the token — the
|
|
171
|
+
> JWT is authoritative. Most `/api/v1/admin/**` and analytics-global routes
|
|
172
|
+
> require an `admin` / `super_admin` role (or a `*` scope) — mint the token
|
|
173
|
+
> accordingly.
|
|
174
|
+
|
|
175
|
+
Override the token source (non-default URL, native app) once at startup:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { configureAuth } from "@elqnt/api-client/browser";
|
|
179
|
+
configureAuth(async () => myTokenProvider()); // URL string or async () => token|null
|
|
180
|
+
// clearGatewayTokenCache() — re-exported from @elqnt/admin/api; call after switching orgs
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Server flow (server actions / SSR)
|
|
184
|
+
|
|
185
|
+
`@elqnt/api-client/server` mints the JWT itself with `jose` — no
|
|
186
|
+
`/api/gateway-token` hop. This is where the secret is injected directly:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import { createServerClient } from "@elqnt/api-client/server";
|
|
190
|
+
|
|
191
|
+
const client = createServerClient({
|
|
192
|
+
gatewayUrl: process.env.API_GATEWAY_URL_INTERNAL!, // in-cluster gateway URL (server-side)
|
|
193
|
+
jwtSecret: process.env.JWT_SECRET!, // the shared gateway secret
|
|
194
|
+
defaultProduct: "eloquent", // gateway reads product from the JWT claim first
|
|
195
|
+
defaultScopes: ["read", "write", "admin"],
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
await client.get("/api/v1/admin/orgs", { orgId, userId });
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
> The admin **hooks are browser-only** (`"use client"`). For SSR/server actions
|
|
202
|
+
> call the gateway paths via `createServerClient` directly — not the hooks.
|
|
203
|
+
|
|
204
|
+
### Env vars
|
|
205
|
+
|
|
206
|
+
| Var | Used by | Purpose |
|
|
207
|
+
|---|---|---|
|
|
208
|
+
| `JWT_SECRET` | `/api/gateway-token` route + `createServerClient` | sign the gateway JWT (same value the gateway validates with) |
|
|
209
|
+
| `API_GATEWAY_URL_INTERNAL` | `createServerClient` `gatewayUrl` | in-cluster gateway URL (server) |
|
|
210
|
+
| `API_GATEWAY_URL_PUBLIC` | SSR layout → `AppConfigProvider` → browser `baseUrl` | public gateway URL, read **at request time** (not `NEXT_PUBLIC_*`) |
|
|
211
|
+
| `ORG_ID` | token route / app config | the acting org all requests are scoped to |
|
|
212
|
+
|
|
213
|
+
### Headers the api layer sets per request
|
|
214
|
+
|
|
215
|
+
| From hook option | Header |
|
|
216
|
+
|---|---|
|
|
217
|
+
| auto token | `Authorization: Bearer <token>` |
|
|
218
|
+
| `orgId` | `X-Org-ID` |
|
|
219
|
+
| `userId` | `X-User-ID` |
|
|
220
|
+
| `userEmail` | `X-User-Email` |
|
|
221
|
+
| `product` (default `"eloquent"`) | `X-Product` |
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Hook options (all hooks)
|
|
226
|
+
|
|
227
|
+
There is **no provider/context** baked into the package. You pass options into
|
|
228
|
+
each hook call. Every hook's options is just `ApiClientOptions`:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
interface ApiClientOptions {
|
|
232
|
+
baseUrl: string; // API Gateway base URL — required
|
|
233
|
+
orgId: string; // required → X-Org-ID
|
|
234
|
+
userId?: string; // → X-User-ID
|
|
235
|
+
userEmail?: string; // → X-User-Email
|
|
236
|
+
product?: string; // → X-Product, defaults to "eloquent"; selects admin_<product> DB
|
|
237
|
+
headers?: Record<string, string>;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
type UseOrgAdminOptions = ApiClientOptions;
|
|
241
|
+
type UseUsersAdminOptions = ApiClientOptions;
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
> **Imperative, not auto-fetching.** Every method returns a Promise you `await`.
|
|
245
|
+
> The hooks expose aggregate `loading` / `error` flags. On failure a method
|
|
246
|
+
> returns its default (`[]`, `null`, `false`) and sets `error` — it does **not**
|
|
247
|
+
> throw.
|
|
248
|
+
>
|
|
249
|
+
> **Callback identity.** Hook methods are recreated when `options` change (the
|
|
250
|
+
> deps array is `[options]`). If you need a stable ref inside `useEffect`/
|
|
251
|
+
> `useCallback`, stash it in a `useRef`:
|
|
252
|
+
> ```ts
|
|
253
|
+
> const listOrgsRef = useRef(listOrgs);
|
|
254
|
+
> listOrgsRef.current = listOrgs;
|
|
255
|
+
> ```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Hook: `useOrgAdmin` — organizations + settings
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
import { useOrgAdmin } from "@elqnt/admin/hooks";
|
|
263
|
+
const orgs = useOrgAdmin({ baseUrl, orgId, userId, userEmail });
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Returns `{ loading, error, ...methods }` (`UseOrgAdminReturn`):
|
|
267
|
+
|
|
268
|
+
| Method | Signature | Resolves to | Endpoint |
|
|
269
|
+
|---|---|---|---|
|
|
270
|
+
| `listOrgs` | `(filter?: ListOrgsFilter) => Promise<Org[]>` | `[]` on error | `GET /api/v1/admin/orgs` (`?status&type&product`) |
|
|
271
|
+
| `getOrg` | `(orgId: string) => Promise<Org \| null>` | `null` | `GET /api/v1/admin/orgs/{orgId}` |
|
|
272
|
+
| `getOrgInfo` | `(orgId: string) => Promise<OrgInfo \| null>` | `null` | `GET /api/v1/admin/orgs/{orgId}/info` |
|
|
273
|
+
| `createOrg` | `(org: Partial<Org> & { product?: string }) => Promise<Org \| null>` | created org | `POST /api/v1/admin/orgs` |
|
|
274
|
+
| `updateOrg` | `(orgId: string, updates: Partial<Org>) => Promise<Org \| null>` | updated org | `PUT /api/v1/admin/orgs/{orgId}` |
|
|
275
|
+
| `deleteOrg` | `(orgId: string) => Promise<boolean>` | `true`/`false` | `DELETE /api/v1/admin/orgs/{orgId}` |
|
|
276
|
+
| `getSettings` | `() => Promise<OrgSettings \| null>` | `null` | `GET /api/v1/admin/orgs/{orgId}` → mapped to `OrgSettings` |
|
|
277
|
+
| `createSettings` | `(settings: Partial<OrgSettings>) => Promise<OrgSettings \| null>` | settings | alias of `updateSettings` (settings rows always exist) |
|
|
278
|
+
| `updateSettings` | `(settings: Partial<OrgSettings>) => Promise<OrgSettings \| null>` | settings | `PUT /api/v1/admin/orgs/{orgId}`, then re-GET |
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
interface ListOrgsFilter { status?: OrgStatusTS; type?: OrgTypeTS; product?: string; }
|
|
282
|
+
// → ?status=active&type=enterprise&product=eloquent (product = which admin_<product> DB)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
> `createOrg`'s `product` field is transient routing metadata (which
|
|
286
|
+
> `admin_<product>` DB to write to) — it is **not** persisted on the org row.
|
|
287
|
+
|
|
288
|
+
**Org settings** (`getSettings`/`createSettings`/`updateSettings`, merged from the
|
|
289
|
+
former `useOrgSettings`) live on the **canonical admin Org row** (since v2.3) and
|
|
290
|
+
are scoped to the hook's `options.orgId`. The hook keeps the legacy `OrgSettings`
|
|
291
|
+
wire shape, but all three hit `/api/v1/admin/orgs/{orgId}` under the hood.
|
|
292
|
+
`updateSettings` PUTs `{ title, description, logoUrl, defaultLang, timezone,
|
|
293
|
+
settings }` (mapped from the `OrgSettings` snake_case fields) and round-trips a
|
|
294
|
+
fresh GET so you get back the full settings shape.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Hook: `useUsersAdmin` — users + invitations
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
import { useUsersAdmin } from "@elqnt/admin/hooks";
|
|
302
|
+
const users = useUsersAdmin({ baseUrl, orgId, userId, userEmail });
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Returns `{ loading, error, ...methods }` (`UseUsersAdminReturn`):
|
|
306
|
+
|
|
307
|
+
| Method | Signature | Resolves to | Endpoint |
|
|
308
|
+
|---|---|---|---|
|
|
309
|
+
| `listUsers` | `() => Promise<User[]>` | `[]` | `GET /api/v1/admin/users` |
|
|
310
|
+
| `getUser` | `(userId: string) => Promise<User \| null>` | `null` | `GET /api/v1/admin/users/{userId}` |
|
|
311
|
+
| `getUserByEmail` | `(email: string) => Promise<User \| null>` | `null` | `GET /api/v1/admin/users/by-email?email=` |
|
|
312
|
+
| `getUserByPhone` | `(phone: string) => Promise<User \| null>` | `null` | `GET /api/v1/admin/users/by-phone?phone=` |
|
|
313
|
+
| `createUser` | `(user: Partial<User>) => Promise<User \| null>` | created user | `POST /api/v1/admin/users` |
|
|
314
|
+
| `updateUser` | `(userId: string, updates: Partial<User>) => Promise<User \| null>` | updated user | `PUT /api/v1/admin/users/{userId}` |
|
|
315
|
+
| `deleteUser` | `(userId: string) => Promise<boolean>` | bool | `DELETE /api/v1/admin/users/{userId}` |
|
|
316
|
+
| `getUserSettings` | `(userId: string) => Promise<UserSettingsResponse \| null>` | `null` | `GET .../users/{userId}/settings` |
|
|
317
|
+
| `updateUserSettings` | `(userId: string, settings: { settings?: UserSettings; notificationPreferences?: NotificationPreferences }) => Promise<UserSettingsResponse \| null>` | settings | `PUT .../users/{userId}/settings` |
|
|
318
|
+
| `listInvites` | `() => Promise<Invite[]>` | `[]` | `GET /api/v1/admin/invites` |
|
|
319
|
+
| `getInvite` | `(inviteId: string) => Promise<Invite \| null>` | `null` | `GET /api/v1/admin/invites/{inviteId}` |
|
|
320
|
+
| `sendInvite` | `(invite: InviteInput) => Promise<Invite \| null>` | invite | `POST /api/v1/admin/invites/single` |
|
|
321
|
+
| `sendInvites` | `(invites: InviteInput[]) => Promise<InvitesResult \| null>` | batch result | `POST /api/v1/admin/invites` body `{ invites }` |
|
|
322
|
+
| `resendInvite` | `(inviteId: string) => Promise<Invite \| null>` | invite | `POST .../invites/{inviteId}/resend` |
|
|
323
|
+
| `revokeInvite` | `(inviteId: string) => Promise<boolean>` | bool | `DELETE /api/v1/admin/invites/{inviteId}` |
|
|
324
|
+
| `acceptInvite` | `(inviteId: string) => Promise<Invite \| null>` | invite | `POST .../invites/{inviteId}/accept` |
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
interface InviteInput { email: string; role: string; }
|
|
328
|
+
interface InvitesResult { sent: InviteSentStatus[]; failed: string[]; nextStep: string; metadata: ResponseMetadata; }
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
> **Invitations** (`listInvites` … `acceptInvite`) were merged in from the former
|
|
332
|
+
> `useInvitesAdmin`. They share this hook's aggregate `loading`/`error`.
|
|
333
|
+
|
|
334
|
+
> **Product analytics** lives in `@elqnt/analytics` now — import
|
|
335
|
+
> `useProductAnalytics` from `@elqnt/analytics/hooks` and its DTOs
|
|
336
|
+
> (`AnalyticsSummary`, `DateFilter`, …) from `@elqnt/analytics/models`. See that
|
|
337
|
+
> package's `SKILL.md`.
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Core types (`@elqnt/admin/models`)
|
|
342
|
+
|
|
343
|
+
> Admin/billing types are tygo-generated (`models/admin.ts`, `models/billing.ts`).
|
|
344
|
+
> `OrgSettings` is re-exported from `@elqnt/agents/models`. (Analytics DTOs moved
|
|
345
|
+
> to `@elqnt/analytics/models`.)
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
interface Org {
|
|
349
|
+
id?: string;
|
|
350
|
+
title: string;
|
|
351
|
+
description?: string;
|
|
352
|
+
logoUrl: string;
|
|
353
|
+
mainDomain: string;
|
|
354
|
+
address: Address;
|
|
355
|
+
defaultLang?: string; timezone?: string;
|
|
356
|
+
settings?: Record<string, any>;
|
|
357
|
+
status: OrgStatusTS; // "active" | "suspended"
|
|
358
|
+
enabled: boolean;
|
|
359
|
+
type: OrgTypeTS; // "self-serve" | "enterprise"
|
|
360
|
+
size?: OrgSizeTS; // "solo" | "small" | "medium" | "large" | "enterprise"
|
|
361
|
+
industry?: string;
|
|
362
|
+
country?: string; // jurisdiction code (e.g. "UAE") → scopes system libraries
|
|
363
|
+
tags?: string[];
|
|
364
|
+
subscription?: OrgSubscription;
|
|
365
|
+
userCount: number;
|
|
366
|
+
onboarding?: OrgOnboarding;
|
|
367
|
+
template?: OrgTemplate; roles: string[];
|
|
368
|
+
metadata?: Record<string, any>;
|
|
369
|
+
createdAt?: number; updatedAt?: number; createdBy?: string; updatedBy?: string;
|
|
370
|
+
}
|
|
371
|
+
interface OrgInfo { id?: string; title: string; logoUrl: string; mainDomain?: string; }
|
|
372
|
+
|
|
373
|
+
interface User {
|
|
374
|
+
id?: string;
|
|
375
|
+
email: string; firstName: string; lastName: string;
|
|
376
|
+
authProviderName: string;
|
|
377
|
+
orgAccess?: UserOrgAccess[];
|
|
378
|
+
enabled?: boolean;
|
|
379
|
+
source?: UserSourceTS; // "signup" | "invite" | "sso" | "api"
|
|
380
|
+
inviteStatus?: InviteStatusTS;
|
|
381
|
+
isSysAdmin?: boolean;
|
|
382
|
+
settings?: UserSettings;
|
|
383
|
+
notificationPreferences?: NotificationPreferences;
|
|
384
|
+
onboarding?: UserOnboarding;
|
|
385
|
+
metadata?: Record<string, any>;
|
|
386
|
+
firstActiveAt?: number; lastActiveAt?: number;
|
|
387
|
+
createdAt?: number; updatedAt?: number; createdBy?: string; updatedBy?: string;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
interface Invite {
|
|
391
|
+
id?: string; orgId: string; email: string; role: string;
|
|
392
|
+
invitedBy: string;
|
|
393
|
+
status: InviteStatusTS; // "pending" | "accepted" | "expired" | "revoked"
|
|
394
|
+
acceptedBy?: string; acceptedAt?: number; expiresAt?: number;
|
|
395
|
+
createdAt?: number; updatedAt?: number;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
interface UserSettings { theme?: string; language?: string; timezone?: string; occupation?: string; company?: string; }
|
|
399
|
+
interface NotificationPreferences {
|
|
400
|
+
pushEnabled: boolean; newChatAssignment: boolean; newMessages: boolean;
|
|
401
|
+
escalations: boolean; urgentOnly: boolean; soundEnabled: boolean;
|
|
402
|
+
doNotDisturb: boolean; dndStart?: string; dndEnd?: string;
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
> Analytics DTOs (`AnalyticsSummary`, `ChatAnalytics`, `OrgAnalytics`,
|
|
407
|
+
> `GlobalChannelSummary`, …) moved to `@elqnt/analytics/models`.
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## API functions (`@elqnt/admin/api`) — for flows with no hook
|
|
412
|
+
|
|
413
|
+
Hooks cover orgs/settings (`useOrgAdmin`) and users/invites (`useUsersAdmin`).
|
|
414
|
+
The **onboarding wizard** and **billing/Stripe** flows have no hook — call their
|
|
415
|
+
api functions directly (with
|
|
416
|
+
`browserApiRequest` token handling already wired in). All take an options object
|
|
417
|
+
(`OnboardingApiOptions` allows a missing `orgId`; the rest take `ApiClientOptions`)
|
|
418
|
+
and return `ApiResponse<T>` (`{ data?, error?, status }` — check `error`).
|
|
419
|
+
|
|
420
|
+
### Onboarding wizard
|
|
421
|
+
|
|
422
|
+
| Fn | Endpoint |
|
|
423
|
+
|---|---|
|
|
424
|
+
| `getOnboardingStatusApi(options)` | `GET /api/v1/onboarding/status` |
|
|
425
|
+
| `startOnboardingApi(options)` | `POST /api/v1/onboarding/start` |
|
|
426
|
+
| `createPaymentSessionApi(params, options)` | `POST /api/v1/onboarding/step/payment` |
|
|
427
|
+
| `createOrganizationApi(org, options)` | `POST /api/v1/onboarding/step/organization` |
|
|
428
|
+
| `sendOnboardingInvitesApi(invites, options)` | `POST /api/v1/onboarding/step/invites` |
|
|
429
|
+
| `createOnboardingKnowledgeApi(knowledge, options)` | `POST /api/v1/onboarding/step/knowledge` |
|
|
430
|
+
| `createOnboardingAgentApi(agent, options)` | `POST /api/v1/onboarding/step/agent` |
|
|
431
|
+
| `createAgentWithSkillsApi(payload, options)` | `POST /api/v1/onboarding/agent-with-skills` |
|
|
432
|
+
| `completeOnboardingApi(options)` | `POST /api/v1/onboarding/complete` |
|
|
433
|
+
| `skipOnboardingStepApi(step, options)` | `POST /api/v1/onboarding/skip-step` |
|
|
434
|
+
| `startOnboardingProvisioningApi(options)` | `POST /api/v1/onboarding/provisioning/start` → `{ orgId }` then connect SSE |
|
|
435
|
+
| `provisionAllOnboardingApi(request, options)` | `POST /api/v1/onboarding/provision-all` — unified collect-then-provision |
|
|
436
|
+
|
|
437
|
+
### Org settings / org artifacts (api-level)
|
|
438
|
+
|
|
439
|
+
| Fn | Endpoint |
|
|
440
|
+
|---|---|
|
|
441
|
+
| `getOrgSettingsApi(options)` | `GET /api/v1/admin/orgs/{orgId}` (→ `OrgSettings`) |
|
|
442
|
+
| `updateOrgSettingsApi(settings, options)` | `PUT /api/v1/admin/orgs/{orgId}`, then re-GET |
|
|
443
|
+
| `createOrgSettingsApi(settings, options)` | *deprecated* alias of `updateOrgSettingsApi` |
|
|
444
|
+
| `updateOrgAgentsApi(agentIds, options)` | `PUT /api/v1/org/agents` |
|
|
445
|
+
| `updateEntityDefinitionsApi(entityNames, options)` | `PUT /api/v1/org/entities` |
|
|
446
|
+
| `updateWorkflowDefinitionsApi(workflowIds, options)` | `PUT /api/v1/org/workflows` |
|
|
447
|
+
|
|
448
|
+
### Billing
|
|
449
|
+
|
|
450
|
+
| Fn | Endpoint |
|
|
451
|
+
|---|---|
|
|
452
|
+
| `getBillingPlansApi(options)` | `GET /api/v1/billing/plans` |
|
|
453
|
+
| `getSubscriptionApi(options)` | `GET /api/v1/billing/subscription` |
|
|
454
|
+
| `getCreditsApi(options)` | `GET /api/v1/billing/credits` |
|
|
455
|
+
| `createCheckoutSessionApi(params, options)` | `POST /api/v1/billing/checkout` |
|
|
456
|
+
| `createPortalSessionApi(params, options)` | `POST /api/v1/billing/portal` |
|
|
457
|
+
| `cancelSubscriptionApi(options)` | `POST /api/v1/billing/subscription/cancel` |
|
|
458
|
+
| `purchaseCreditsApi(params, options)` | `POST /api/v1/billing/credits/purchase` |
|
|
459
|
+
|
|
460
|
+
> **Deprecated — do not use.** `provisionDefaultAgentsApi`, `provisionEntitiesApi`,
|
|
461
|
+
> `provisionWorkflowsApi` (per-org artifact provisioning is gone — use the domain
|
|
462
|
+
> packages `@elqnt/agents/api`, `@elqnt/entity/api`, etc.). The lower-level CRUD
|
|
463
|
+
> api fns (`listOrgsApi`, `createUserApi`, `sendInvitesApi`, …) exist too, but
|
|
464
|
+
> prefer the hooks for app code. (Analytics api fns moved to
|
|
465
|
+
> `@elqnt/analytics/api`.)
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## The app layer
|
|
470
|
+
|
|
471
|
+
Wrap the hook in **one** app file and a context, so components speak the domain
|
|
472
|
+
type, not the gateway. Read `baseUrl`/`orgId` from SSR (an `AppConfig` context
|
|
473
|
+
fed by `API_GATEWAY_URL_PUBLIC` at request time), never a `NEXT_PUBLIC_*` var.
|
|
474
|
+
|
|
475
|
+
```tsx
|
|
476
|
+
// contexts/app-config-context.tsx
|
|
477
|
+
"use client";
|
|
478
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
479
|
+
|
|
480
|
+
export interface AppConfig { apiGatewayUrl: string; orgId: string; }
|
|
481
|
+
const Ctx = createContext<AppConfig | undefined>(undefined);
|
|
482
|
+
export function AppConfigProvider({ children, initialConfig }: { children: ReactNode; initialConfig: AppConfig; }) {
|
|
483
|
+
return <Ctx.Provider value={initialConfig}>{children}</Ctx.Provider>;
|
|
484
|
+
}
|
|
485
|
+
export function useAppConfig() {
|
|
486
|
+
const c = useContext(Ctx);
|
|
487
|
+
if (!c) throw new Error("useAppConfig must be used within AppConfigProvider");
|
|
488
|
+
return c;
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
```tsx
|
|
493
|
+
// app/layout.tsx (server component) — read the gateway URL at request time
|
|
494
|
+
import { AppConfigProvider } from "@/contexts/app-config-context";
|
|
495
|
+
const apiGatewayUrl = process.env.API_GATEWAY_URL_PUBLIC || "http://localhost:8080"; // NOT NEXT_PUBLIC_
|
|
496
|
+
export default async function Layout({ children }: { children: React.ReactNode }) {
|
|
497
|
+
return (
|
|
498
|
+
<AppConfigProvider initialConfig={{ apiGatewayUrl, orgId: process.env.ORG_ID! }}>
|
|
499
|
+
{children}
|
|
500
|
+
</AppConfigProvider>
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
```tsx
|
|
506
|
+
// contexts/orgs-context.tsx — one shared hook instance for the orgs subtree
|
|
507
|
+
"use client";
|
|
508
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
509
|
+
import { useOrgAdmin, type UseOrgAdminReturn } from "@elqnt/admin/hooks";
|
|
510
|
+
import { useAppConfig } from "@/contexts/app-config-context";
|
|
511
|
+
|
|
512
|
+
const OrgsCtx = createContext<UseOrgAdminReturn | undefined>(undefined);
|
|
513
|
+
export function OrgsProvider({ children }: { children: ReactNode }) {
|
|
514
|
+
const { apiGatewayUrl, orgId } = useAppConfig();
|
|
515
|
+
const value = useOrgAdmin({ baseUrl: apiGatewayUrl, orgId }); // single instance
|
|
516
|
+
return <OrgsCtx.Provider value={value}>{children}</OrgsCtx.Provider>;
|
|
517
|
+
}
|
|
518
|
+
export function useOrgsContext(): UseOrgAdminReturn {
|
|
519
|
+
const c = useContext(OrgsCtx);
|
|
520
|
+
if (!c) throw new Error("useOrgsContext must be used within OrgsProvider");
|
|
521
|
+
return c;
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
```tsx
|
|
526
|
+
// components/orgs/orgs-table.tsx — consume the context, never import @elqnt/admin
|
|
527
|
+
"use client";
|
|
528
|
+
import { useEffect, useState } from "react";
|
|
529
|
+
import { useOrgsContext } from "@/contexts/orgs-context";
|
|
530
|
+
import type { Org } from "@elqnt/admin/models";
|
|
531
|
+
|
|
532
|
+
export function OrgsTable() {
|
|
533
|
+
const { listOrgs, loading, error } = useOrgsContext();
|
|
534
|
+
const [rows, setRows] = useState<Org[]>([]);
|
|
535
|
+
useEffect(() => { listOrgs({ status: "active" }).then(setRows); }, [listOrgs]);
|
|
536
|
+
if (error) return <p>{error}</p>;
|
|
537
|
+
return <ul>{rows.map((o) => <li key={o.id}>{o.title} — {o.userCount} users</li>)}</ul>;
|
|
538
|
+
}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
**Rule of thumb:** `@elqnt/admin` is imported in exactly the context/app-hook
|
|
542
|
+
file. Everything above it speaks `Org` / `User` / `Invite`. Typed `Use*Return`
|
|
543
|
+
imports (`UseOrgAdminReturn`, …) are how the context carries the hook shape.
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
## Gotchas
|
|
548
|
+
|
|
549
|
+
- **Two hooks, browser-only.** `useOrgAdmin` (org CRUD + settings) and
|
|
550
|
+
`useUsersAdmin` (user CRUD + invitations). Both are `"use client"`. For
|
|
551
|
+
SSR/server actions, call the gateway paths via `createServerClient`, not the
|
|
552
|
+
hooks. (Product analytics moved to `@elqnt/analytics` — `useProductAnalytics`.)
|
|
553
|
+
- **Methods don't throw.** They return defaults (`[]`, `null`, `false`) and set
|
|
554
|
+
`error`. Check `error` / null results; don't wrap in try/catch expecting throws.
|
|
555
|
+
- **No provider in the package.** Options go into every hook call — which is why
|
|
556
|
+
your app builds `AppConfigProvider` (inject `baseUrl`/`orgId` once via SSR) and
|
|
557
|
+
a per-domain context.
|
|
558
|
+
- **Callbacks are unstable.** Methods are recreated when `options` change; use a
|
|
559
|
+
`useRef` if you need a stable reference inside `useEffect` deps.
|
|
560
|
+
- **`product` selects the DB, it is not stored.** The gateway resolves product
|
|
561
|
+
from the JWT claim (server) and `X-Product` (browser). `listOrgs`/`createOrg`'s
|
|
562
|
+
`product` field is transient routing metadata — mismatched product = wrong
|
|
563
|
+
`admin_<product>` DB.
|
|
564
|
+
- **Org settings live on the Org row.** `useOrgAdmin`'s
|
|
565
|
+
`getSettings`/`updateSettings` and `get/updateOrgSettingsApi` hit
|
|
566
|
+
`/api/v1/admin/orgs/{orgId}` and re-map to the legacy `OrgSettings` shape —
|
|
567
|
+
there is no separate settings endpoint.
|
|
568
|
+
- **Per-org artifact provisioning is gone.** `provisionDefaultAgentsApi` /
|
|
569
|
+
`provisionEntitiesApi` / `provisionWorkflowsApi` are deprecated; use the domain
|
|
570
|
+
packages. See `docs/_db_v2/03_system_org_split.md`.
|