web-mojo 2.2.55 → 2.2.57

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/dist/admin.es.js CHANGED
@@ -10,7 +10,7 @@ import { M as MapView, a as MetricsCountryMapView } from "./chunks/MetricsCountr
10
10
  import { M as Model, C as Collection } from "./chunks/Collection-DaiL0uGl.js";
11
11
  import { L as LightboxGallery, P as PDFViewer } from "./chunks/PDFViewer-EJ9cOfPF.js";
12
12
  import { W } from "./chunks/WebApp-6qvqmOts.js";
13
- import { B, a, V, b, c, d } from "./chunks/version-ZWtWuDxk.js";
13
+ import { B, a, V, b, c, d } from "./chunks/version-OyPGnx30.js";
14
14
  class AdminHeaderView extends View {
15
15
  constructor(options = {}) {
16
16
  super({
@@ -2249,6 +2249,310 @@ class GeoLocatedIPTablePage extends TablePage {
2249
2249
  }
2250
2250
  }
2251
2251
  }
2252
+ class ApiKey extends Model {
2253
+ constructor(data = {}, options = {}) {
2254
+ super(data, {
2255
+ endpoint: "/api/group/apikey",
2256
+ ...options
2257
+ });
2258
+ }
2259
+ }
2260
+ class ApiKeyList extends Collection {
2261
+ constructor(options = {}) {
2262
+ super({
2263
+ ModelClass: ApiKey,
2264
+ endpoint: "/api/group/apikey",
2265
+ size: 25,
2266
+ ...options
2267
+ });
2268
+ }
2269
+ }
2270
+ const ApiKeyForms = {
2271
+ create: {
2272
+ title: "Create API Key",
2273
+ fields: [
2274
+ {
2275
+ name: "name",
2276
+ type: "text",
2277
+ label: "Name",
2278
+ placeholder: "Mobile App v2",
2279
+ required: true,
2280
+ columns: 12,
2281
+ help: "A descriptive name to identify this key."
2282
+ },
2283
+ {
2284
+ name: "group",
2285
+ type: "number",
2286
+ label: "Group ID",
2287
+ required: true,
2288
+ columns: 6,
2289
+ help: "The group this key is scoped to."
2290
+ },
2291
+ {
2292
+ name: "permissions",
2293
+ type: "textarea",
2294
+ label: "Permissions (JSON)",
2295
+ placeholder: '{"view_orders": true, "create_orders": true}',
2296
+ columns: 12,
2297
+ help: "JSON dict of permissions to grant. Leave empty for no permissions."
2298
+ }
2299
+ ]
2300
+ },
2301
+ edit: {
2302
+ title: "Edit API Key",
2303
+ fields: [
2304
+ {
2305
+ name: "name",
2306
+ type: "text",
2307
+ label: "Name",
2308
+ required: true,
2309
+ columns: 12
2310
+ },
2311
+ {
2312
+ name: "is_active",
2313
+ type: "switch",
2314
+ label: "Active",
2315
+ columns: 12,
2316
+ help: "Deactivate to revoke access without deleting the key."
2317
+ },
2318
+ {
2319
+ name: "permissions",
2320
+ type: "textarea",
2321
+ label: "Permissions (JSON)",
2322
+ columns: 12,
2323
+ help: "JSON dict of granted permissions."
2324
+ }
2325
+ ]
2326
+ }
2327
+ };
2328
+ class ApiKeyView extends View {
2329
+ constructor(options = {}) {
2330
+ super({
2331
+ className: "api-key-view",
2332
+ ...options
2333
+ });
2334
+ this.model = options.model || new ApiKey(options.data || {});
2335
+ this.template = `
2336
+ <div class="api-key-view-container">
2337
+ <!-- Header -->
2338
+ <div class="d-flex justify-content-between align-items-start mb-4">
2339
+ <!-- Left: Icon & Identity -->
2340
+ <div class="d-flex align-items-center gap-3">
2341
+ <div class="fs-1 text-primary">
2342
+ <i class="bi bi-key"></i>
2343
+ </div>
2344
+ <div>
2345
+ <h3 class="mb-1">{{model.name|default('Unnamed Key')}}</h3>
2346
+ <div class="text-muted small">
2347
+ ID: {{model.id}}
2348
+ <span class="mx-2">|</span>
2349
+ Group: {{model.group.name|default(model.group)}}
2350
+ </div>
2351
+ <div class="mt-1">
2352
+ <span class="badge {{model.is_active|boolean('bg-success','bg-secondary')}}">
2353
+ {{model.is_active|boolean('Active','Inactive')}}
2354
+ </span>
2355
+ </div>
2356
+ </div>
2357
+ </div>
2358
+
2359
+ <!-- Right: Meta & Actions -->
2360
+ <div class="d-flex align-items-start gap-4">
2361
+ <div class="text-end">
2362
+ <div class="text-muted small">Created</div>
2363
+ <div>{{model.created|datetime}}</div>
2364
+ </div>
2365
+ <div data-container="apikey-context-menu"></div>
2366
+ </div>
2367
+ </div>
2368
+
2369
+ <!-- Details -->
2370
+ <div class="list-group mb-3">
2371
+ <div class="list-group-item">
2372
+ <h6 class="mb-1 text-muted">Token Preview</h6>
2373
+ <p class="mb-1 font-monospace small text-muted">
2374
+ The raw token is only shown once at creation time.
2375
+ </p>
2376
+ </div>
2377
+ <div class="list-group-item">
2378
+ <h6 class="mb-1 text-muted">Permissions</h6>
2379
+ {{#model.permissions}}
2380
+ <pre class="mb-0 small">{{model.permissions|json}}</pre>
2381
+ {{/model.permissions}}
2382
+ {{^model.permissions}}
2383
+ <span class="text-muted small">No permissions granted</span>
2384
+ {{/model.permissions}}
2385
+ </div>
2386
+ {{#model.limits}}
2387
+ <div class="list-group-item">
2388
+ <h6 class="mb-1 text-muted">Rate Limit Overrides</h6>
2389
+ <pre class="mb-0 small">{{model.limits|json}}</pre>
2390
+ </div>
2391
+ {{/model.limits}}
2392
+ <div class="list-group-item">
2393
+ <h6 class="mb-1 text-muted">Usage</h6>
2394
+ <p class="mb-0 small text-muted">
2395
+ Include in requests as:
2396
+ <code>Authorization: apikey &lt;token&gt;</code>
2397
+ </p>
2398
+ </div>
2399
+ </div>
2400
+ </div>
2401
+ `;
2402
+ }
2403
+ async onInit() {
2404
+ const isActive = this.model.get("is_active");
2405
+ const apiKeyMenu = new ContextMenu({
2406
+ containerId: "apikey-context-menu",
2407
+ className: "context-menu-view header-menu-absolute",
2408
+ context: this.model,
2409
+ config: {
2410
+ icon: "bi-three-dots-vertical",
2411
+ items: [
2412
+ { label: "Edit", action: "edit-key", icon: "bi-pencil" },
2413
+ isActive ? { label: "Deactivate", action: "deactivate-key", icon: "bi-x-circle" } : { label: "Activate", action: "activate-key", icon: "bi-check-circle" },
2414
+ { type: "divider" },
2415
+ { label: "Delete Key", action: "delete-key", icon: "bi-trash", danger: true }
2416
+ ]
2417
+ }
2418
+ });
2419
+ this.addChild(apiKeyMenu);
2420
+ }
2421
+ async onActionEditKey() {
2422
+ const app = this.getApp();
2423
+ const resp = await app.showModelForm({
2424
+ title: `Edit API Key — ${this.model.get("name")}`,
2425
+ model: this.model,
2426
+ formConfig: ApiKeyForms.edit
2427
+ });
2428
+ if (resp) {
2429
+ this.render();
2430
+ }
2431
+ }
2432
+ async onActionDeactivateKey() {
2433
+ const app = this.getApp();
2434
+ const confirmed = await app.confirm({
2435
+ title: "Deactivate API Key",
2436
+ message: `Deactivate "${this.model.get("name")}"? Requests using this key will be rejected.`,
2437
+ confirmLabel: "Deactivate",
2438
+ confirmClass: "btn-warning"
2439
+ });
2440
+ if (!confirmed) return;
2441
+ app.showLoading();
2442
+ const resp = await this.model.save({ is_active: false });
2443
+ app.hideLoading();
2444
+ if (resp && resp.success !== false) {
2445
+ app.toast.success("API key deactivated");
2446
+ this.render();
2447
+ } else {
2448
+ app.toast.error("Failed to deactivate key");
2449
+ }
2450
+ }
2451
+ async onActionActivateKey() {
2452
+ const app = this.getApp();
2453
+ app.showLoading();
2454
+ const resp = await this.model.save({ is_active: true });
2455
+ app.hideLoading();
2456
+ if (resp && resp.success !== false) {
2457
+ app.toast.success("API key activated");
2458
+ this.render();
2459
+ } else {
2460
+ app.toast.error("Failed to activate key");
2461
+ }
2462
+ }
2463
+ async onActionDeleteKey() {
2464
+ const app = this.getApp();
2465
+ const confirmed = await app.confirm({
2466
+ title: "Delete API Key",
2467
+ message: `Permanently delete "${this.model.get("name")}"? This cannot be undone.`,
2468
+ confirmLabel: "Delete",
2469
+ confirmClass: "btn-danger"
2470
+ });
2471
+ if (!confirmed) return;
2472
+ app.showLoading();
2473
+ const resp = await this.model.delete();
2474
+ app.hideLoading();
2475
+ if (resp && resp.success !== false) {
2476
+ app.toast.success("API key deleted");
2477
+ this.emit("deleted", { model: this.model });
2478
+ } else {
2479
+ app.toast.error("Failed to delete key");
2480
+ }
2481
+ }
2482
+ }
2483
+ ApiKey.VIEW_CLASS = ApiKeyView;
2484
+ ApiKey.ADD_FORM = ApiKeyForms.create;
2485
+ ApiKey.EDIT_FORM = ApiKeyForms.edit;
2486
+ class ApiKeyTablePage extends TablePage {
2487
+ constructor(options = {}) {
2488
+ super({
2489
+ ...options,
2490
+ name: "admin_api_keys",
2491
+ pageName: "API Keys",
2492
+ router: "admin/api-keys",
2493
+ Collection: ApiKeyList,
2494
+ itemViewClass: ApiKeyView,
2495
+ viewDialogOptions: {
2496
+ header: false,
2497
+ size: "lg"
2498
+ },
2499
+ columns: [
2500
+ { key: "id", label: "ID", width: "70px", sortable: true, class: "text-muted" },
2501
+ { key: "name", label: "Name", sortable: true },
2502
+ { key: "group.name", label: "Group", sortable: true, formatter: "default('—')" },
2503
+ {
2504
+ key: "is_active",
2505
+ label: "Status",
2506
+ formatter: "boolean('Active|bg-success','Inactive|bg-secondary')|badge",
2507
+ width: "100px"
2508
+ },
2509
+ { key: "created", label: "Created", formatter: "datetime", sortable: true }
2510
+ ],
2511
+ selectable: true,
2512
+ searchable: true,
2513
+ sortable: true,
2514
+ filterable: true,
2515
+ paginated: true,
2516
+ showRefresh: true,
2517
+ showAdd: true,
2518
+ showExport: false,
2519
+ addButtonLabel: "New API Key",
2520
+ emptyMessage: "No API keys found.",
2521
+ tableOptions: {
2522
+ striped: true,
2523
+ bordered: false,
2524
+ hover: true,
2525
+ responsive: false
2526
+ }
2527
+ });
2528
+ }
2529
+ // Override to intercept and show the one-time token after model.save()
2530
+ async onActionAdd() {
2531
+ const app = this.getApp();
2532
+ const model = new ApiKey();
2533
+ const result = await app.showForm({
2534
+ model,
2535
+ ...ApiKeyForms.create
2536
+ });
2537
+ if (!result) return;
2538
+ const resp = await model.save(result);
2539
+ if (!resp?.data?.status) {
2540
+ app.showError(resp?.data?.error || "Failed to create API key");
2541
+ return;
2542
+ }
2543
+ const token = resp.data?.data?.token;
2544
+ await app.showAlert({
2545
+ title: "API Key Created — Save Your Token",
2546
+ message: token ? `Copy this token now. It will not be shown again.
2547
+
2548
+ ${token}` : "API key created successfully.",
2549
+ type: token ? "warning" : "success",
2550
+ size: "lg"
2551
+ });
2552
+ this.collection.add(model);
2553
+ this.tableView?.refresh();
2554
+ }
2555
+ }
2252
2556
  class IncidentDashboardHeader extends View {
2253
2557
  constructor(options = {}) {
2254
2558
  super({
@@ -9527,6 +9831,7 @@ function registerSystemPages(app, addToMenu = true) {
9527
9831
  app.registerPage("system/push/devices", PushDeviceTablePage, { permissions: ["manage_users"] });
9528
9832
  app.registerPage("system/phonehub/numbers", PhoneNumberTablePage, { permissions: ["manage_users"] });
9529
9833
  app.registerPage("system/phonehub/sms", SMSTablePage, { permissions: ["manage_users"] });
9834
+ app.registerPage("system/api-keys", ApiKeyTablePage, { permissions: ["manage_groups", "manage_group"] });
9530
9835
  if (addToMenu && app.sidebar && app.sidebar.getMenuConfig) {
9531
9836
  const adminMenuConfig = app.sidebar.getMenuConfig("system");
9532
9837
  if (adminMenuConfig && adminMenuConfig.items) {
@@ -9628,6 +9933,12 @@ function registerSystemPages(app, addToMenu = true) {
9628
9933
  route: "?page=system/metrics/permissions",
9629
9934
  icon: "bi-bar-chart-line",
9630
9935
  permissions: ["manage_metrics"]
9936
+ },
9937
+ {
9938
+ text: "API Keys",
9939
+ route: "?page=system/api-keys",
9940
+ icon: "bi-key",
9941
+ permissions: ["manage_groups", "manage_group"]
9631
9942
  }
9632
9943
  ]
9633
9944
  },
@@ -9729,6 +10040,8 @@ function registerSystemPages(app, addToMenu = true) {
9729
10040
  }
9730
10041
  export {
9731
10042
  AdminDashboardPage,
10043
+ ApiKeyTablePage,
10044
+ ApiKeyView,
9732
10045
  B as BUILD_TIME,
9733
10046
  DeviceView,
9734
10047
  EmailDomainTablePage,