PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 2.0.11 All 82 releases
yatra / resources / js / api / team-api.ts

team-api.ts in Yatra – Travel Booking & Tour Operator Software trunk, at resources/js/api/team-api.ts

376 lines 11.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { apiClient } from "../lib/api-client";
2
3 /* -------------------------------------------------------------------------- */
4 /* Types */
5 /* -------------------------------------------------------------------------- */
6
7 /** Module-level gate state — drives the upgrade card / module-disabled card. */
8 export interface TeamMeta {
9 is_agency_active: boolean;
10 is_module_enabled: boolean;
11 upgrade_url: string;
12 docs_url: string;
13 audit_log_enabled: boolean;
14 invitation_default_expiry_seconds: number;
15 }
16
17 /** One capability in the registry. */
18 export interface CapabilityDef {
19 category: string;
20 sensitivity: "low" | "medium" | "high" | "critical";
21 module: string;
22 label: string;
23 }
24
25 export interface CapabilityRegistryResponse {
26 capabilities: Record<string, CapabilityDef>;
27 by_category: Record<string, Record<string, CapabilityDef>>;
28 version: number;
29 }
30
31 /** Per-user scope assignment. */
32 export interface UserScopes {
33 destinations: number[];
34 activities: number[];
35 trips: number[];
36 categories: number[];
37 }
38
39 export interface TeamRole {
40 slug: string;
41 display_name: string;
42 description: string;
43 is_system: boolean;
44 capabilities: string[];
45 capability_count: number;
46 member_count: number;
47 }
48
49 export interface TeamUser {
50 id: number;
51 display_name: string;
52 user_login: string;
53 email: string;
54 roles: string[];
55 primary_role: string;
56 is_wp_admin: boolean;
57 effective_caps: string[];
58 caps_grant: string[];
59 caps_revoke: string[];
60 scopes: UserScopes;
61 has_scope: boolean;
62 last_login: string;
63 /**
64 * Time-windowed access. Unix seconds; 0 = permanent (no expiry).
65 * When the timestamp is in the past, the server has soft-stripped
66 * yatra_* caps in-memory and the hourly ExpiryCron is queued to
67 * hard-remove the role + grants on its next run.
68 */
69 expires_at: number;
70 is_expired: boolean;
71 }
72
73 export interface TeamInvitation {
74 id: string;
75 email: string;
76 role_slug: string;
77 invited_by: number;
78 created_at: number;
79 expires_at: number;
80 status: "pending" | "accepted" | "revoked" | "expired";
81 accepted_at?: number;
82 accepted_user_id?: number;
83 }
84
85 export interface AuditLogRow {
86 id: number;
87 occurred_at: string;
88 actor_user_id: number | null;
89 actor_display_name: string;
90 actor_ip: string | null;
91 actor_user_agent: string | null;
92 action: string;
93 capability: string | null;
94 entity_type: string | null;
95 entity_id: number | null;
96 context: Record<string, unknown>;
97 result: "allowed" | "denied";
98 }
99
100 export interface TeamUserWritePayload {
101 role_slug?: string;
102 caps_grant?: string[];
103 caps_revoke?: string[];
104 scopes?: Partial<UserScopes>;
105 /**
106 * Time-windowed access. Accept unix seconds, ISO-8601, or null/0
107 * to clear. Owners cannot set this on themselves — the server
108 * rejects with 409. Omit the field to leave expiry unchanged.
109 */
110 expires_at?: number | string | null;
111 }
112
113 export interface SendInvitationPayload {
114 email: string;
115 role: string;
116 expires_in?: number;
117 scopes?: Partial<UserScopes>;
118 }
119
120 /* -------------------------------------------------------------------------- */
121 /* Client */
122 /* -------------------------------------------------------------------------- */
123
124 export const teamApi = {
125 getMeta: () => apiClient.get("/team/meta") as Promise<TeamMeta>,
126
127 listCapabilities: () =>
128 apiClient.get("/team/capabilities") as Promise<CapabilityRegistryResponse>,
129
130 listRoles: () =>
131 apiClient.get("/team/roles") as Promise<{ data: TeamRole[] }>,
132
133 createRole: (payload: {
134 display_name: string;
135 capabilities: string[];
136 slug?: string;
137 }) =>
138 apiClient.post("/team/roles", payload) as Promise<{
139 data: TeamRole;
140 message: string;
141 }>,
142
143 getRole: (slug: string) =>
144 apiClient.get(`/team/roles/${encodeURIComponent(slug)}`) as Promise<{
145 data: TeamRole;
146 }>,
147
148 updateRole: (
149 slug: string,
150 payload: { display_name?: string; capabilities?: string[] },
151 ) =>
152 apiClient.put(
153 `/team/roles/${encodeURIComponent(slug)}`,
154 payload,
155 ) as Promise<{ data: TeamRole; message: string }>,
156
157 deleteRole: (slug: string, reassignTo: string | null = null) =>
158 apiClient.delete(
159 `/team/roles/${encodeURIComponent(slug)}`,
160 reassignTo ? { data: { reassign_to: reassignTo } } : undefined,
161 ) as Promise<{ message: string }>,
162
163 listUsers: () =>
164 apiClient.get("/team/users") as Promise<{ data: TeamUser[] }>,
165
166 /** WP users not yet on the Yatra team — for the "Add existing user"
167 * picker. `q` is an optional type-ahead query (search by login /
168 * email / display name). */
169 listAvailableUsers: (q = "", limit = 50) =>
170 apiClient.get("/team/users/available", {
171 params: q ? { q, limit } : { limit },
172 }) as Promise<{
173 data: Array<{
174 id: number;
175 display_name: string;
176 email: string;
177 login: string;
178 }>;
179 }>,
180
181 /** Create a brand-new WP user + assign a Yatra role in one shot.
182 * Sister flow to invitations — invitations send an email that
183 * creates the user on accept; this one provisions immediately. */
184 createUser: (payload: {
185 email: string;
186 role_slug: string;
187 first_name?: string;
188 last_name?: string;
189 username?: string;
190 password?: string;
191 send_reset_email?: boolean;
192 scopes?: Partial<UserScopes>;
193 }) =>
194 apiClient.post("/team/users/create", payload) as Promise<{
195 data: TeamUser;
196 message: string;
197 }>,
198
199 getUser: (id: number) =>
200 apiClient.get(`/team/users/${id}`) as Promise<{ data: TeamUser }>,
201
202 updateUser: (id: number, payload: TeamUserWritePayload) =>
203 apiClient.put(`/team/users/${id}`, payload) as Promise<{
204 data: TeamUser;
205 message: string;
206 }>,
207
208 forceLogout: (id: number) =>
209 apiClient.post(`/team/users/${id}/force-logout`, {}) as Promise<{
210 message: string;
211 }>,
212
213 removeUser: (id: number) =>
214 apiClient.delete(`/team/users/${id}`) as Promise<{ message: string }>,
215
216 /**
217 * Bulk operations on members.
218 *
219 * Response shape includes per-id success/failure so the UI can
220 * highlight rows that didn't complete (e.g. last-admin guard
221 * blocking a remove). `ok_count` is the convenience aggregate.
222 */
223 /** Curated role-creation templates served read-only from the server. */
224 listRoleTemplates: () =>
225 apiClient.get("/team/role-templates") as Promise<{
226 data: Array<{
227 id: string;
228 label: string;
229 description: string;
230 capabilities: string[];
231 }>;
232 }>,
233
234 /**
235 * Module-level settings. Currently surfaces a single forward-looking
236 * toggle: what should happen to non-admin team members' Yatra access
237 * if the operator ever turns the Team & Access module off?
238 *
239 * Returns an object (not a flat boolean) so future settings can be
240 * added without rev-bumping the response shape.
241 */
242 getSettings: () =>
243 apiClient.get("/team/settings") as Promise<{
244 data: {
245 keep_access_on_module_disable: boolean;
246 /** Comma-separated CIDR list. Empty = no restriction. Empty
247 * string is the safe default — operators opt in deliberately
248 * because misconfiguration locks staff out. WordPress
249 * administrators are always exempt regardless. */
250 login_ip_allowlist: string;
251 };
252 }>,
253
254 /** Partial update — every key is optional. Backend persists only
255 * the keys present and writes an audit row when the diff is
256 * meaningful (e.g. login_ip_allowlist normalizes to a different
257 * list of CIDRs). */
258 updateSettings: (payload: {
259 keep_access_on_module_disable?: boolean;
260 login_ip_allowlist?: string;
261 }) =>
262 apiClient.put("/team/settings", payload) as Promise<{
263 data: {
264 keep_access_on_module_disable: boolean;
265 login_ip_allowlist: string;
266 };
267 message: string;
268 }>,
269
270 bulkUsers: (payload: {
271 action: "change_role" | "remove" | "force_logout" | "set_expiry";
272 user_ids: number[];
273 role_slug?: string;
274 expires_at?: number | string | null;
275 }) =>
276 apiClient.post("/team/users/bulk", payload) as Promise<{
277 data: {
278 results: Array<{ id: number; ok: boolean; error?: string }>;
279 ok_count: number;
280 fail_count: number;
281 };
282 message: string;
283 }>,
284
285 listInvitations: () =>
286 apiClient.get("/team/invitations") as Promise<{
287 data: Record<string, TeamInvitation>;
288 }>,
289
290 sendInvitation: (payload: SendInvitationPayload) =>
291 apiClient.post("/team/invitations", payload) as Promise<{
292 data: TeamInvitation & { accept_url: string; email_sent: boolean };
293 message: string;
294 }>,
295
296 /**
297 * Revoke or delete an invitation.
298 *
299 * purge=false (default): the invitation must be `pending`. Marks
300 * it `revoked` (the magic-link token is invalidated) and keeps
301 * the row so the operator can still see it in the list.
302 * purge=true: deletes the row entirely. If the invitation was
303 * still pending, it's revoked first so the token can never be
304 * redeemed after the record is gone.
305 */
306 revokeInvitation: (id: string, opts: { purge?: boolean } = {}) =>
307 apiClient.delete(
308 `/team/invitations/${encodeURIComponent(id)}${opts.purge ? "?purge=1" : ""}`,
309 ) as Promise<{
310 data: {
311 /** revoked | revoked_and_deleted | deleted */
312 action: "revoked" | "revoked_and_deleted" | "deleted";
313 invitation_id: string;
314 previous_status?: string;
315 };
316 message: string;
317 }>,
318
319 acceptInvitation: (token: string) =>
320 apiClient.post("/team/invitations/accept", { token }) as Promise<{
321 user_id: number;
322 role_slug: string;
323 message: string;
324 }>,
325
326 listAuditLog: (
327 filters: {
328 page?: number;
329 per_page?: number;
330 actor_user_id?: number;
331 action?: string;
332 entity_type?: string;
333 entity_id?: number;
334 result?: "allowed" | "denied";
335 date_from?: string;
336 date_to?: string;
337 } = {},
338 ) =>
339 apiClient.get("/team/audit-log", { params: filters }) as Promise<{
340 data: AuditLogRow[];
341 total: number;
342 page: number;
343 per_page: number;
344 }>,
345
346 auditFacets: () =>
347 apiClient.get("/team/audit-log/facets") as Promise<{
348 actions: string[];
349 entity_types: string[];
350 }>,
351
352 /**
353 * Wipe every audit-log entry. Server requires `confirm: true` in the
354 * body as a safety belt against accidental DELETEs. The wipe itself
355 * is recorded as a new audit entry so the operator who cleared
356 * history is documented.
357 */
358 clearAuditLog: () =>
359 apiClient.delete("/team/audit-log", {
360 data: { confirm: true },
361 }) as Promise<{
362 data: { rows_deleted: number };
363 message: string;
364 }>,
365
366 /**
367 * Delete a specific set of audit-log rows by id. Server caps at 500
368 * ids per request. The deletion is recorded as a new audit entry.
369 */
370 bulkDeleteAuditLog: (ids: number[]) =>
371 apiClient.post("/team/audit-log/bulk-delete", { ids }) as Promise<{
372 data: { rows_deleted: number; requested: number };
373 message: string;
374 }>,
375 };
376