web-mojo 2.2.55 → 2.2.56

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-MS6IWlRk.js";
14
14
  class AdminHeaderView extends View {
15
15
  constructor(options = {}) {
16
16
  super({
@@ -2249,6 +2249,311 @@ 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 resp = await Dialog$1.showModelForm({
2423
+ title: `Edit API Key — ${this.model.get("name")}`,
2424
+ model: this.model,
2425
+ formConfig: ApiKeyForms.edit
2426
+ });
2427
+ if (resp) {
2428
+ this.render();
2429
+ }
2430
+ }
2431
+ async onActionDeactivateKey() {
2432
+ const confirmed = await Dialog$1.confirm({
2433
+ title: "Deactivate API Key",
2434
+ message: `Deactivate "${this.model.get("name")}"? Requests using this key will be rejected.`,
2435
+ confirmLabel: "Deactivate",
2436
+ confirmClass: "btn-warning"
2437
+ });
2438
+ if (!confirmed) return;
2439
+ const app = this.getApp();
2440
+ app.showLoading();
2441
+ const resp = await this.model.save({ is_active: false });
2442
+ app.hideLoading();
2443
+ if (resp && resp.success !== false) {
2444
+ app.toast.success("API key deactivated");
2445
+ this.render();
2446
+ } else {
2447
+ app.toast.error("Failed to deactivate key");
2448
+ }
2449
+ }
2450
+ async onActionActivateKey() {
2451
+ const app = this.getApp();
2452
+ app.showLoading();
2453
+ const resp = await this.model.save({ is_active: true });
2454
+ app.hideLoading();
2455
+ if (resp && resp.success !== false) {
2456
+ app.toast.success("API key activated");
2457
+ this.render();
2458
+ } else {
2459
+ app.toast.error("Failed to activate key");
2460
+ }
2461
+ }
2462
+ async onActionDeleteKey() {
2463
+ const confirmed = await Dialog$1.confirm({
2464
+ title: "Delete API Key",
2465
+ message: `Permanently delete "${this.model.get("name")}"? This cannot be undone.`,
2466
+ confirmLabel: "Delete",
2467
+ confirmClass: "btn-danger"
2468
+ });
2469
+ if (!confirmed) return;
2470
+ const app = this.getApp();
2471
+ app.showLoading();
2472
+ const resp = await this.model.delete();
2473
+ app.hideLoading();
2474
+ if (resp && resp.success !== false) {
2475
+ app.toast.success("API key deleted");
2476
+ this.emit("deleted", { model: this.model });
2477
+ } else {
2478
+ app.toast.error("Failed to delete key");
2479
+ }
2480
+ }
2481
+ }
2482
+ ApiKey.VIEW_CLASS = ApiKeyView;
2483
+ class ApiKeyTablePage extends TablePage {
2484
+ constructor(options = {}) {
2485
+ super({
2486
+ ...options,
2487
+ name: "admin_api_keys",
2488
+ pageName: "API Keys",
2489
+ router: "admin/api-keys",
2490
+ Collection: ApiKeyList,
2491
+ itemViewClass: ApiKeyView,
2492
+ viewDialogOptions: {
2493
+ header: false,
2494
+ size: "lg"
2495
+ },
2496
+ columns: [
2497
+ { key: "id", label: "ID", width: "70px", sortable: true, class: "text-muted" },
2498
+ { key: "name", label: "Name", sortable: true },
2499
+ { key: "group.name", label: "Group", sortable: true, formatter: "default('—')" },
2500
+ {
2501
+ key: "is_active",
2502
+ label: "Status",
2503
+ formatter: "boolean('Active|bg-success','Inactive|bg-secondary')|badge",
2504
+ width: "100px"
2505
+ },
2506
+ { key: "created", label: "Created", formatter: "datetime", sortable: true }
2507
+ ],
2508
+ selectable: true,
2509
+ searchable: true,
2510
+ sortable: true,
2511
+ filterable: true,
2512
+ paginated: true,
2513
+ showRefresh: true,
2514
+ showAdd: true,
2515
+ showExport: false,
2516
+ addButtonLabel: "New API Key",
2517
+ emptyMessage: "No API keys found.",
2518
+ tableOptions: {
2519
+ striped: true,
2520
+ bordered: false,
2521
+ hover: true,
2522
+ responsive: false
2523
+ }
2524
+ });
2525
+ }
2526
+ /**
2527
+ * Override onAdd to show the create form and display the token on success.
2528
+ * The token is only returned at creation time — display it in an alert.
2529
+ */
2530
+ async onAdd() {
2531
+ const app = this.getApp();
2532
+ const data = await Dialog$1.showForm({
2533
+ title: "Create API Key",
2534
+ formConfig: ApiKeyForms.create,
2535
+ size: "md"
2536
+ });
2537
+ if (!data) return;
2538
+ app.showLoading();
2539
+ const resp = await app.rest.POST("/api/group/apikey", data);
2540
+ app.hideLoading();
2541
+ if (resp && resp.status) {
2542
+ const token = resp.data?.token;
2543
+ await Dialog$1.alert({
2544
+ title: "API Key Created",
2545
+ message: token ? `Copy this token now — it will not be shown again.
2546
+
2547
+ ${token}` : "API key created successfully.",
2548
+ type: token ? "warning" : "success",
2549
+ size: "lg"
2550
+ });
2551
+ this.collection.fetch();
2552
+ } else {
2553
+ app.toast.error(resp?.error || "Failed to create API key");
2554
+ }
2555
+ }
2556
+ }
2252
2557
  class IncidentDashboardHeader extends View {
2253
2558
  constructor(options = {}) {
2254
2559
  super({
@@ -9527,6 +9832,7 @@ function registerSystemPages(app, addToMenu = true) {
9527
9832
  app.registerPage("system/push/devices", PushDeviceTablePage, { permissions: ["manage_users"] });
9528
9833
  app.registerPage("system/phonehub/numbers", PhoneNumberTablePage, { permissions: ["manage_users"] });
9529
9834
  app.registerPage("system/phonehub/sms", SMSTablePage, { permissions: ["manage_users"] });
9835
+ app.registerPage("system/api-keys", ApiKeyTablePage, { permissions: ["manage_groups", "manage_group"] });
9530
9836
  if (addToMenu && app.sidebar && app.sidebar.getMenuConfig) {
9531
9837
  const adminMenuConfig = app.sidebar.getMenuConfig("system");
9532
9838
  if (adminMenuConfig && adminMenuConfig.items) {
@@ -9628,6 +9934,12 @@ function registerSystemPages(app, addToMenu = true) {
9628
9934
  route: "?page=system/metrics/permissions",
9629
9935
  icon: "bi-bar-chart-line",
9630
9936
  permissions: ["manage_metrics"]
9937
+ },
9938
+ {
9939
+ text: "API Keys",
9940
+ route: "?page=system/api-keys",
9941
+ icon: "bi-key",
9942
+ permissions: ["manage_groups", "manage_group"]
9631
9943
  }
9632
9944
  ]
9633
9945
  },
@@ -9729,6 +10041,8 @@ function registerSystemPages(app, addToMenu = true) {
9729
10041
  }
9730
10042
  export {
9731
10043
  AdminDashboardPage,
10044
+ ApiKeyTablePage,
10045
+ ApiKeyView,
9732
10046
  B as BUILD_TIME,
9733
10047
  DeviceView,
9734
10048
  EmailDomainTablePage,