web-mojo 2.1.980 → 2.1.1043
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.cjs.js +1 -1
- package/dist/admin.cjs.js.map +1 -1
- package/dist/admin.es.js +740 -33
- package/dist/admin.es.js.map +1 -1
- package/dist/auth.cjs.js +1 -1
- package/dist/auth.cjs.js.map +1 -1
- package/dist/auth.es.js +2 -2
- package/dist/auth.es.js.map +1 -1
- package/dist/charts.cjs.js +1 -1
- package/dist/charts.es.js +2 -2
- package/dist/chunks/ChatView-CGBaudUc.js +2 -0
- package/dist/chunks/ChatView-CGBaudUc.js.map +1 -0
- package/dist/chunks/{ChatView-rAfKBqDw.js → ChatView-DguKw-gR.js} +83 -13
- package/dist/chunks/ChatView-DguKw-gR.js.map +1 -0
- package/dist/chunks/{Collection-DaTm-2LH.js → Collection-YRfGoT73.js} +2 -2
- package/dist/chunks/{Collection-DaTm-2LH.js.map → Collection-YRfGoT73.js.map} +1 -1
- package/dist/chunks/{ContextMenu-BuEqfeZS.js → ContextMenu-B4_YS0G8.js} +2 -2
- package/dist/chunks/{ContextMenu-BuEqfeZS.js.map → ContextMenu-B4_YS0G8.js.map} +1 -1
- package/dist/chunks/{Dialog-CENvQT9n.js → Dialog-BiVgKzSK.js} +3 -3
- package/dist/chunks/{Dialog-CENvQT9n.js.map → Dialog-BiVgKzSK.js.map} +1 -1
- package/dist/chunks/{Dialog-DZqbxTsP.js → Dialog-DmIPK_Bi.js} +2 -2
- package/dist/chunks/{Dialog-DZqbxTsP.js.map → Dialog-DmIPK_Bi.js.map} +1 -1
- package/dist/chunks/{FormView-095xPgXv.js → FormView-BClEkzmE.js} +93 -5
- package/dist/chunks/FormView-BClEkzmE.js.map +1 -0
- package/dist/chunks/FormView-nulck4nL.js +3 -0
- package/dist/chunks/FormView-nulck4nL.js.map +1 -0
- package/dist/chunks/{ListView-BrsQ26R6.js → ListView-BMNhd5-B.js} +2 -2
- package/dist/chunks/{ListView-BrsQ26R6.js.map → ListView-BMNhd5-B.js.map} +1 -1
- package/dist/chunks/{MetricsMiniChartWidget-CVRinHn4.js → MetricsMiniChartWidget-CCroU6BZ.js} +2 -2
- package/dist/chunks/{MetricsMiniChartWidget-CVRinHn4.js.map → MetricsMiniChartWidget-CCroU6BZ.js.map} +1 -1
- package/dist/chunks/{MetricsMiniChartWidget-Dez6aHCT.js → MetricsMiniChartWidget-Esvv-lFp.js} +2 -2
- package/dist/chunks/{MetricsMiniChartWidget-Dez6aHCT.js.map → MetricsMiniChartWidget-Esvv-lFp.js.map} +1 -1
- package/dist/chunks/{PDFViewer-CwzGbdOv.js → PDFViewer-D4uo3oiA.js} +2 -2
- package/dist/chunks/{PDFViewer-CwzGbdOv.js.map → PDFViewer-D4uo3oiA.js.map} +1 -1
- package/dist/chunks/{PDFViewer-dAEmy7XJ.js → PDFViewer-NeL91Gon.js} +2 -2
- package/dist/chunks/{PDFViewer-dAEmy7XJ.js.map → PDFViewer-NeL91Gon.js.map} +1 -1
- package/dist/chunks/{TopNav-Cj9_6vRl.js → TopNav-23B5R-dl.js} +2 -2
- package/dist/chunks/{TopNav-Cj9_6vRl.js.map → TopNav-23B5R-dl.js.map} +1 -1
- package/dist/chunks/{TopNav-Celew98v.js → TopNav-DC8oGpHp.js} +4 -4
- package/dist/chunks/{TopNav-Celew98v.js.map → TopNav-DC8oGpHp.js.map} +1 -1
- package/dist/chunks/{WebApp-7JKRzXMJ.js → WebApp-C1vcdSuu.js} +13 -13
- package/dist/chunks/WebApp-C1vcdSuu.js.map +1 -0
- package/dist/chunks/WebApp-CpxtmTk0.js +2 -0
- package/dist/chunks/WebApp-CpxtmTk0.js.map +1 -0
- package/dist/docit.cjs.js +1 -1
- package/dist/docit.es.js +5 -5
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +11 -11
- package/dist/lightbox.cjs.js +1 -1
- package/dist/lightbox.es.js +3 -3
- package/dist/map.es.js +1 -1
- package/dist/timeline.es.js +2 -2
- package/package.json +1 -1
- package/dist/chunks/ChatView-BXois2IL.js +0 -2
- package/dist/chunks/ChatView-BXois2IL.js.map +0 -1
- package/dist/chunks/ChatView-rAfKBqDw.js.map +0 -1
- package/dist/chunks/FormView-095xPgXv.js.map +0 -1
- package/dist/chunks/FormView-DGA3I2IL.js +0 -3
- package/dist/chunks/FormView-DGA3I2IL.js.map +0 -1
- package/dist/chunks/WebApp-7JKRzXMJ.js.map +0 -1
- package/dist/chunks/WebApp-9HUBOegS.js +0 -2
- package/dist/chunks/WebApp-9HUBOegS.js.map +0 -1
package/dist/admin.es.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { P as Page } from "./chunks/Page-Deq4y2Kq.js";
|
|
2
|
-
import { V as View, M as MOJOUtils } from "./chunks/Rest-CS4jRCAs.js";
|
|
2
|
+
import { V as View, M as MOJOUtils, r as rest } from "./chunks/Rest-CS4jRCAs.js";
|
|
3
3
|
import "./chunks/WebSocketClient-D-5DJoMX.js";
|
|
4
|
-
import Dialog$1 from "./chunks/Dialog-
|
|
5
|
-
import { M as MetricsChart, c as MetricsMiniChartWidget, P as PieChart } from "./chunks/MetricsMiniChartWidget-
|
|
6
|
-
import { b as TablePage, j as EmailDomainForms, i as EmailDomainList, E as EmailDomain, m as MailboxForms, l as MailboxList, k as Mailbox, q as EmailTemplate, c as TabView, s as EmailTemplateForms, r as EmailTemplateList, I as IncidentEvent, A as IncidentEventForms, z as IncidentEventList, v as FileManagerForms, u as FileManagerList, w as File, T as TableView, y as FileForms, x as FileList, au as GeoLocatedIP,
|
|
4
|
+
import Dialog$1 from "./chunks/Dialog-BiVgKzSK.js";
|
|
5
|
+
import { M as MetricsChart, c as MetricsMiniChartWidget, P as PieChart } from "./chunks/MetricsMiniChartWidget-CCroU6BZ.js";
|
|
6
|
+
import { b as TablePage, j as EmailDomainForms, i as EmailDomainList, E as EmailDomain, m as MailboxForms, l as MailboxList, k as Mailbox, q as EmailTemplate, c as TabView, s as EmailTemplateForms, r as EmailTemplateList, I as IncidentEvent, A as IncidentEventForms, z as IncidentEventList, v as FileManagerForms, u as FileManagerList, w as File, T as TableView, y as FileForms, x as FileList, au as GeoLocatedIP, ae as LogList, av as GeoLocatedIPList, af as MemberList, ax as TicketList, D as IncidentList, Y as IncidentStats, O as IncidentHistoryList, N as IncidentHistory, B as Incident, C as ChatView, G as IncidentForms, a2 as Job, a8 as JobEventList, a6 as JobLogList, a4 as JobForms, a3 as JobList, ab as JobRunnerList, a9 as JobsEngineStats, ac as JobRunnerForms, aa as JobRunner, ad as Log, M as Member, ag as MemberForms, ah as MetricsPermission, aj as MetricsForms, ai as MetricsPermissionList, as as PushConfigForms, ap as PushConfigList, ar as PushDeliveryList, al as PushDeviceList, at as PushTemplateForms, an as PushTemplateList, R as RuleSet, _ as MatchByOptions, Z as BundleByOptions, W as RuleList, Q as RuleSetList, h as S3BucketForms, g as S3BucketList, n as SentMessage, o as SentMessageList, az as TicketNoteList, ay as TicketNote, aw as Ticket, aA as TicketForms, aB as TicketCategories } from "./chunks/ChatView-DguKw-gR.js";
|
|
7
7
|
import DataView from "./chunks/DataView-OUqaLmGB.js";
|
|
8
|
-
import { C as ContextMenu, a as Group, G as GroupList, b as GroupForms, f as UserDevice, i as UserDeviceLocationList, g as UserDeviceList, U as User, e as UserDataView, d as UserForms, c as UserList } from "./chunks/ContextMenu-
|
|
9
|
-
import { C as Collection } from "./chunks/Collection-
|
|
10
|
-
import { L as LightboxGallery, P as PDFViewer } from "./chunks/PDFViewer-
|
|
11
|
-
import { a as applyFileDropMixin, F as FormView } from "./chunks/FormView-
|
|
8
|
+
import { C as ContextMenu, a as Group, G as GroupList, b as GroupForms, f as UserDevice, i as UserDeviceLocationList, g as UserDeviceList, U as User, e as UserDataView, d as UserForms, c as UserList } from "./chunks/ContextMenu-B4_YS0G8.js";
|
|
9
|
+
import { C as Collection, M as Model } from "./chunks/Collection-YRfGoT73.js";
|
|
10
|
+
import { L as LightboxGallery, P as PDFViewer } from "./chunks/PDFViewer-NeL91Gon.js";
|
|
11
|
+
import { a as applyFileDropMixin, F as FormView } from "./chunks/FormView-BClEkzmE.js";
|
|
12
12
|
import { MapView } from "./map.es.js";
|
|
13
|
-
import { B, a, V, b, c, d, W } from "./chunks/WebApp-
|
|
13
|
+
import { B, a, V, b, c, d, W } from "./chunks/WebApp-C1vcdSuu.js";
|
|
14
14
|
class AdminHeaderView extends View {
|
|
15
15
|
constructor(options = {}) {
|
|
16
16
|
super({
|
|
@@ -1604,7 +1604,7 @@ class FileTablePage extends TablePage {
|
|
|
1604
1604
|
*/
|
|
1605
1605
|
async onActionAdd(event, element) {
|
|
1606
1606
|
event.preventDefault();
|
|
1607
|
-
const Dialog2 = (await import("./chunks/Dialog-
|
|
1607
|
+
const Dialog2 = (await import("./chunks/Dialog-BiVgKzSK.js")).default;
|
|
1608
1608
|
const formData = await Dialog2.showForm({
|
|
1609
1609
|
title: "Upload File",
|
|
1610
1610
|
size: "md",
|
|
@@ -1720,17 +1720,43 @@ class GeoIPView extends View {
|
|
|
1720
1720
|
<div>
|
|
1721
1721
|
<h3 class="mb-1">{{model.ip_address}}</h3>
|
|
1722
1722
|
<div class="text-muted small">
|
|
1723
|
-
{{model.country_name|default('Unknown Location')}}
|
|
1723
|
+
{{model.city|default('Unknown Location')}}, {{model.country_name|default('Unknown Location')}}
|
|
1724
1724
|
</div>
|
|
1725
1725
|
<div class="text-muted small mt-1">
|
|
1726
|
-
|
|
1726
|
+
ISP: {{model.isp|capitalize}}
|
|
1727
1727
|
</div>
|
|
1728
1728
|
</div>
|
|
1729
1729
|
</div>
|
|
1730
1730
|
|
|
1731
|
-
<!-- Right Side: Actions -->
|
|
1732
|
-
<div class="d-flex align-items-
|
|
1733
|
-
|
|
1731
|
+
<!-- Right Side: Risk Summary + Actions -->
|
|
1732
|
+
<div class="d-flex align-items-start gap-4">
|
|
1733
|
+
<!-- Risk summary -->
|
|
1734
|
+
<div class="text-end">
|
|
1735
|
+
<div class="d-flex align-items-baseline justify-content-end gap-2">
|
|
1736
|
+
<span class="text-muted">Risk:</span>
|
|
1737
|
+
<span class="fw-bold fs-4
|
|
1738
|
+
{{#model.is_threat}} text-danger {{/model.is_threat}}
|
|
1739
|
+
{{#model.is_suspicious}} text-warning {{/model.is_suspicious}}
|
|
1740
|
+
{{^model.is_threat}}{{^model.is_suspicious}} text-success {{/model.is_suspicious}}{{/model.is_threat}}
|
|
1741
|
+
">{{#model.threat_level}}{{model.threat_level|capitalize}}{{/model.threat_level}}{{^model.threat_level}}Unknown{{/model.threat_level}}</span>
|
|
1742
|
+
</div>
|
|
1743
|
+
<div class="mt-1 small d-flex align-items-center justify-content-end gap-2">
|
|
1744
|
+
<span class="text-muted">Score:</span>
|
|
1745
|
+
<span class="fw-semibold">{{model.risk_score|default('—')}}</span>
|
|
1746
|
+
</div>
|
|
1747
|
+
<div class="mt-1 d-flex align-items-center justify-content-end gap-2">
|
|
1748
|
+
<i class="bi bi-shield-lock {{#model.is_tor}}fs-4 text-success{{/model.is_tor}}{{^model.is_tor}}text-muted{{/model.is_tor}}" data-bs-toggle="tooltip" title="TOR exit"></i>
|
|
1749
|
+
<i class="bi bi-shield {{#model.is_vpn}}fs-4 text-success{{/model.is_vpn}}{{^model.is_vpn}}text-muted{{/model.is_vpn}}" data-bs-toggle="tooltip" title="VPN detected"></i>
|
|
1750
|
+
<i class="bi bi-cloud {{#model.is_cloud}}fs-4 text-success{{/model.is_cloud}}{{^model.is_cloud}}text-muted{{/model.is_cloud}}" data-bs-toggle="tooltip" title="Cloud provider"></i>
|
|
1751
|
+
<i class="bi bi-hdd-stack {{#model.is_datacenter}}fs-4 text-success{{/model.is_datacenter}}{{^model.is_datacenter}}text-muted{{/model.is_datacenter}}" data-bs-toggle="tooltip" title="Datacenter"></i>
|
|
1752
|
+
<i class="bi bi-phone {{#model.is_mobile}}fs-4 text-success{{/model.is_mobile}}{{^model.is_mobile}}text-muted{{/model.is_mobile}}" data-bs-toggle="tooltip" title="Mobile connection"></i>
|
|
1753
|
+
<i class="bi bi-diagram-3 {{#model.is_proxy}}fs-4 text-success{{/model.is_proxy}}{{^model.is_proxy}}text-muted{{/model.is_proxy}}" data-bs-toggle="tooltip" title="Proxy"></i>
|
|
1754
|
+
</div>
|
|
1755
|
+
</div>
|
|
1756
|
+
<!-- Actions: context menu aligned to top (not vertically centered) -->
|
|
1757
|
+
<div class="d-flex align-items-start">
|
|
1758
|
+
<div data-container="geoip-context-menu"></div>
|
|
1759
|
+
</div>
|
|
1734
1760
|
</div>
|
|
1735
1761
|
</div>
|
|
1736
1762
|
|
|
@@ -1759,25 +1785,41 @@ class GeoIPView extends View {
|
|
|
1759
1785
|
{ name: "longitude", label: "Longitude", cols: 4 }
|
|
1760
1786
|
]
|
|
1761
1787
|
});
|
|
1762
|
-
this.
|
|
1788
|
+
this.networkView = new DataView({
|
|
1763
1789
|
model: this.model,
|
|
1764
1790
|
className: "p-3",
|
|
1765
1791
|
showEmptyValues: true,
|
|
1766
1792
|
emptyValueText: "—",
|
|
1767
1793
|
columns: 2,
|
|
1768
1794
|
fields: [
|
|
1769
|
-
{ name: "
|
|
1770
|
-
{ name: "is_tor", label: "TOR Exit Node", cols: 4 },
|
|
1795
|
+
{ name: "is_tor", label: "TOR Exit Node", formatter: "yesnoicon", cols: 4 },
|
|
1771
1796
|
{ name: "is_vpn", label: "VPN", formatter: "yesnoicon", cols: 4 },
|
|
1772
1797
|
{ name: "is_proxy", label: "Proxy", formatter: "yesnoicon", cols: 4 },
|
|
1773
1798
|
{ name: "is_cloud", label: "Cloud Provider", formatter: "yesnoicon", cols: 4 },
|
|
1774
1799
|
{ name: "is_datacenter", label: "Datacenter", formatter: "yesnoicon", cols: 4 },
|
|
1800
|
+
{ name: "is_mobile", label: "Mobile", formatter: "yesnoicon", cols: 4 },
|
|
1801
|
+
{ name: "mobile_carrier", label: "Mobile Carrier", cols: 8 },
|
|
1775
1802
|
{ name: "asn", label: "ASN", cols: 4 },
|
|
1776
|
-
{ name: "asn_org", label: "ASN Organization", cols:
|
|
1777
|
-
{ name: "isp", label: "ISP", cols:
|
|
1803
|
+
{ name: "asn_org", label: "ASN Organization", cols: 8 },
|
|
1804
|
+
{ name: "isp", label: "ISP", cols: 12 },
|
|
1778
1805
|
{ name: "connection_type", label: "Connection Type", cols: 6 }
|
|
1779
1806
|
]
|
|
1780
1807
|
});
|
|
1808
|
+
this.riskView = new DataView({
|
|
1809
|
+
model: this.model,
|
|
1810
|
+
className: "p-3",
|
|
1811
|
+
showEmptyValues: true,
|
|
1812
|
+
emptyValueText: "—",
|
|
1813
|
+
columns: 2,
|
|
1814
|
+
fields: [
|
|
1815
|
+
{ name: "threat_level", label: "Threat Level", cols: 6 },
|
|
1816
|
+
{ name: "risk_score", label: "Risk Score", cols: 6 },
|
|
1817
|
+
{ name: "is_threat", label: "Threat", formatter: "yesnoicon", cols: 6 },
|
|
1818
|
+
{ name: "is_suspicious", label: "Suspicious", formatter: "yesnoicon", cols: 6 },
|
|
1819
|
+
{ name: "is_known_attacker", label: "Known Attacker", formatter: "yesnoicon", cols: 6 },
|
|
1820
|
+
{ name: "is_known_abuser", label: "Known Abuser", formatter: "yesnoicon", cols: 6 }
|
|
1821
|
+
]
|
|
1822
|
+
});
|
|
1781
1823
|
this.metadataView = new DataView({
|
|
1782
1824
|
model: this.model,
|
|
1783
1825
|
className: "p-3",
|
|
@@ -1789,12 +1831,77 @@ class GeoIPView extends View {
|
|
|
1789
1831
|
{ name: "provider", label: "Data Provider", formatter: "capitalize", cols: 6 },
|
|
1790
1832
|
{ name: "created", label: "Created", formatter: "datetime", cols: 6 },
|
|
1791
1833
|
{ name: "modified", label: "Last Modified", formatter: "datetime", cols: 6 },
|
|
1792
|
-
{ name: "
|
|
1834
|
+
{ name: "last_seen", label: "Last Seen", formatter: "datetime", cols: 6 },
|
|
1835
|
+
{ name: "expires_at", label: "Expires", formatter: "datetime", cols: 6 }
|
|
1836
|
+
]
|
|
1837
|
+
});
|
|
1838
|
+
const eventsCollection = new IncidentEventList({
|
|
1839
|
+
params: {
|
|
1840
|
+
size: 5,
|
|
1841
|
+
source_ip: this.model.get("ip_address")
|
|
1842
|
+
}
|
|
1843
|
+
});
|
|
1844
|
+
this.eventsView = new TableView({
|
|
1845
|
+
collection: eventsCollection,
|
|
1846
|
+
hideActivePillNames: ["source_ip"],
|
|
1847
|
+
columns: [
|
|
1848
|
+
{ key: "id", label: "ID", sortable: true, width: "40px" },
|
|
1849
|
+
{ key: "created", label: "Date", formatter: "datetime", sortable: true, width: "150px" },
|
|
1850
|
+
{ key: "category|badge", label: "Category" },
|
|
1851
|
+
{ key: "title", label: "Title" }
|
|
1852
|
+
]
|
|
1853
|
+
});
|
|
1854
|
+
const logsCollection = new LogList({
|
|
1855
|
+
params: {
|
|
1856
|
+
size: 5,
|
|
1857
|
+
ip: this.model.get("ip_address")
|
|
1858
|
+
}
|
|
1859
|
+
});
|
|
1860
|
+
this.logsView = new TableView({
|
|
1861
|
+
collection: logsCollection,
|
|
1862
|
+
permissions: "view_logs",
|
|
1863
|
+
hideActivePillNames: ["ip"],
|
|
1864
|
+
columns: [
|
|
1865
|
+
{
|
|
1866
|
+
key: "created",
|
|
1867
|
+
label: "Timestamp",
|
|
1868
|
+
sortable: true,
|
|
1869
|
+
formatter: "epoch|datetime",
|
|
1870
|
+
filter: {
|
|
1871
|
+
name: "created",
|
|
1872
|
+
type: "daterange",
|
|
1873
|
+
startName: "dr_start",
|
|
1874
|
+
endName: "dr_end",
|
|
1875
|
+
fieldName: "dr_field",
|
|
1876
|
+
label: "Date Range",
|
|
1877
|
+
format: "YYYY-MM-DD",
|
|
1878
|
+
displayFormat: "MMM DD, YYYY",
|
|
1879
|
+
separator: " to "
|
|
1880
|
+
}
|
|
1881
|
+
},
|
|
1882
|
+
{
|
|
1883
|
+
key: "level",
|
|
1884
|
+
label: "Level",
|
|
1885
|
+
sortable: true,
|
|
1886
|
+
filter: {
|
|
1887
|
+
type: "select",
|
|
1888
|
+
options: [
|
|
1889
|
+
{ value: "info", label: "Info" },
|
|
1890
|
+
{ value: "warning", label: "Warning" },
|
|
1891
|
+
{ value: "error", label: "Error" }
|
|
1892
|
+
]
|
|
1893
|
+
}
|
|
1894
|
+
},
|
|
1895
|
+
{ key: "kind", label: "Kind", filter: { type: "text" } },
|
|
1896
|
+
{ name: "log", label: "Log" }
|
|
1793
1897
|
]
|
|
1794
1898
|
});
|
|
1795
1899
|
const tabs = {
|
|
1796
1900
|
"Location": this.detailsView,
|
|
1797
|
-
"
|
|
1901
|
+
"Network": this.networkView,
|
|
1902
|
+
"Risk & Reputation": this.riskView,
|
|
1903
|
+
"Events": this.eventsView,
|
|
1904
|
+
"Logs": this.logsView,
|
|
1798
1905
|
"Metadata": this.metadataView
|
|
1799
1906
|
};
|
|
1800
1907
|
if (this.hasCoordinates) {
|
|
@@ -1851,6 +1958,19 @@ class GeoIPView extends View {
|
|
|
1851
1958
|
});
|
|
1852
1959
|
this.addChild(geoIPMenu);
|
|
1853
1960
|
}
|
|
1961
|
+
async onAfterRender() {
|
|
1962
|
+
await super.onAfterRender();
|
|
1963
|
+
if (window.bootstrap && window.bootstrap.Tooltip && this.element) {
|
|
1964
|
+
const tooltipTriggerList = this.element.querySelectorAll('[data-bs-toggle="tooltip"]');
|
|
1965
|
+
tooltipTriggerList.forEach((el) => {
|
|
1966
|
+
const existing = window.bootstrap.Tooltip.getInstance(el);
|
|
1967
|
+
if (existing && typeof existing.dispose === "function") {
|
|
1968
|
+
existing.dispose();
|
|
1969
|
+
}
|
|
1970
|
+
new window.bootstrap.Tooltip(el);
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1854
1974
|
async onActionEditLocation() {
|
|
1855
1975
|
const resp = await Dialog$1.showModelForm({
|
|
1856
1976
|
title: `Edit Location - ${this.model.get("ip_address")}`,
|
|
@@ -1888,6 +2008,10 @@ class GeoIPView extends View {
|
|
|
1888
2008
|
await this.model.save({ refresh: true });
|
|
1889
2009
|
this.getApp()?.toast?.info("Refresh request sent for " + this.model.get("ip_address"));
|
|
1890
2010
|
}
|
|
2011
|
+
async onActionThreatAnalysis() {
|
|
2012
|
+
await this.model.save({ threat_analysis: true });
|
|
2013
|
+
this.getApp()?.toast?.info("Requesting threat analysis for " + this.model.get("ip_address"));
|
|
2014
|
+
}
|
|
1891
2015
|
async onActionViewOnMap() {
|
|
1892
2016
|
if (this.hasCoordinates) {
|
|
1893
2017
|
const lat = this.model.get("latitude");
|
|
@@ -1961,7 +2085,7 @@ class GeoLocatedIPTablePage extends TablePage {
|
|
|
1961
2085
|
clickAction: "view",
|
|
1962
2086
|
// Toolbar
|
|
1963
2087
|
showRefresh: true,
|
|
1964
|
-
showAdd:
|
|
2088
|
+
showAdd: true,
|
|
1965
2089
|
showExport: true,
|
|
1966
2090
|
// Empty state
|
|
1967
2091
|
emptyMessage: "No GeoIP records found.",
|
|
@@ -1971,9 +2095,34 @@ class GeoLocatedIPTablePage extends TablePage {
|
|
|
1971
2095
|
bordered: false,
|
|
1972
2096
|
hover: true,
|
|
1973
2097
|
responsive: false
|
|
2098
|
+
},
|
|
2099
|
+
tableViewOptions: {
|
|
2100
|
+
addButtonLabel: "Lookup IP",
|
|
2101
|
+
onAdd: (evt) => {
|
|
2102
|
+
evt.preventDefault();
|
|
2103
|
+
this.onLookup();
|
|
2104
|
+
}
|
|
1974
2105
|
}
|
|
1975
2106
|
});
|
|
1976
2107
|
}
|
|
2108
|
+
async onLookup() {
|
|
2109
|
+
const data = await this.getApp().showForm({
|
|
2110
|
+
title: "Lookup IP",
|
|
2111
|
+
fields: [
|
|
2112
|
+
{
|
|
2113
|
+
name: "ip",
|
|
2114
|
+
type: "text",
|
|
2115
|
+
required: true
|
|
2116
|
+
}
|
|
2117
|
+
]
|
|
2118
|
+
});
|
|
2119
|
+
if (data && data.ip) {
|
|
2120
|
+
const model = await GeoLocatedIP.lookup(data.ip);
|
|
2121
|
+
if (model) {
|
|
2122
|
+
this.tableView._onRowView({ model });
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
1977
2126
|
}
|
|
1978
2127
|
class GroupView extends View {
|
|
1979
2128
|
constructor(options = {}) {
|
|
@@ -2194,8 +2343,10 @@ class GroupView extends View {
|
|
|
2194
2343
|
Collection: GroupList,
|
|
2195
2344
|
labelField: "name",
|
|
2196
2345
|
itemTemplate: `
|
|
2197
|
-
<div class="
|
|
2346
|
+
<div class="ms-2">
|
|
2347
|
+
<div class="fs-7">{{model.name}}</div>
|
|
2198
2348
|
<div class="fs-8 text-muted">{{model.kind}}</div>
|
|
2349
|
+
</div>
|
|
2199
2350
|
`,
|
|
2200
2351
|
valueField: "id",
|
|
2201
2352
|
enableSearch: true,
|
|
@@ -2329,15 +2480,7 @@ class GroupTablePage extends TablePage {
|
|
|
2329
2480
|
striped: true,
|
|
2330
2481
|
bordered: false,
|
|
2331
2482
|
hover: true,
|
|
2332
|
-
responsive: false
|
|
2333
|
-
toolbarButtons: [
|
|
2334
|
-
{
|
|
2335
|
-
label: "Add Multiple",
|
|
2336
|
-
icon: "bi bi-plus-circle",
|
|
2337
|
-
action: "add-multiple",
|
|
2338
|
-
className: "btn-success"
|
|
2339
|
-
}
|
|
2340
|
-
]
|
|
2483
|
+
responsive: false
|
|
2341
2484
|
}
|
|
2342
2485
|
});
|
|
2343
2486
|
}
|
|
@@ -8474,6 +8617,548 @@ class UserTablePage extends TablePage {
|
|
|
8474
8617
|
return false;
|
|
8475
8618
|
}
|
|
8476
8619
|
}
|
|
8620
|
+
class PhoneNumber extends Model {
|
|
8621
|
+
constructor(data = {}, options = {}) {
|
|
8622
|
+
super(data, {
|
|
8623
|
+
endpoint: "/api/phonehub/number",
|
|
8624
|
+
...options
|
|
8625
|
+
});
|
|
8626
|
+
}
|
|
8627
|
+
/**
|
|
8628
|
+
* Normalize an arbitrary phone_number string to E.164 format.
|
|
8629
|
+
* @param {string} phoneNumber - Raw phone number input.
|
|
8630
|
+
* @param {string} [countryCode='US'] - Optional country code (default US).
|
|
8631
|
+
* @returns {Promise<{success: boolean, phone_number?: string, data?: object, error?: string, response?: any}>}
|
|
8632
|
+
*/
|
|
8633
|
+
static async normalize(phoneNumber, countryCode = "US") {
|
|
8634
|
+
const url = "/api/phonehub/number/normalize";
|
|
8635
|
+
const payload = {
|
|
8636
|
+
phone_number: phoneNumber
|
|
8637
|
+
};
|
|
8638
|
+
if (countryCode) payload.country_code = countryCode;
|
|
8639
|
+
const resp = await rest.POST(url, payload);
|
|
8640
|
+
const body = resp?.data ?? resp;
|
|
8641
|
+
const ok = body?.status === true || body?.success === true;
|
|
8642
|
+
if (ok) {
|
|
8643
|
+
const normalized = body?.data?.phone_number ?? body?.phone_number;
|
|
8644
|
+
return { success: true, phone_number: normalized, data: body?.data ?? body, response: resp };
|
|
8645
|
+
}
|
|
8646
|
+
return { success: false, error: body?.error || "Normalization failed", response: resp };
|
|
8647
|
+
}
|
|
8648
|
+
/**
|
|
8649
|
+
* Lookup phone number details (carrier, line_type, owner, etc.)
|
|
8650
|
+
* @param {string} phoneNumber - E.164 or raw phone number.
|
|
8651
|
+
* @param {object} [options] - { force_refresh?: boolean, group?: number }
|
|
8652
|
+
* @returns {Promise<{success: boolean, model?: PhoneNumber, data?: object, error?: string, response?: any}>}
|
|
8653
|
+
*/
|
|
8654
|
+
static async lookup(phoneNumber, options = {}) {
|
|
8655
|
+
const url = "/api/phonehub/number/lookup";
|
|
8656
|
+
const resp = await rest.POST(url, {
|
|
8657
|
+
phone_number: phoneNumber,
|
|
8658
|
+
...options
|
|
8659
|
+
});
|
|
8660
|
+
const body = resp?.data ?? resp;
|
|
8661
|
+
const ok = body?.status === true || body?.success === true;
|
|
8662
|
+
if (ok) {
|
|
8663
|
+
const data = body?.data ?? {};
|
|
8664
|
+
const model = new PhoneNumber(data, { endpoint: "/api/phonehub/number" });
|
|
8665
|
+
return { success: true, model, data, response: resp };
|
|
8666
|
+
}
|
|
8667
|
+
return { success: false, error: body?.error || "Phone lookup failed", response: resp };
|
|
8668
|
+
}
|
|
8669
|
+
}
|
|
8670
|
+
class PhoneNumberList extends Collection {
|
|
8671
|
+
constructor(options = {}) {
|
|
8672
|
+
super({
|
|
8673
|
+
ModelClass: PhoneNumber,
|
|
8674
|
+
endpoint: "/api/phonehub/number",
|
|
8675
|
+
size: 10,
|
|
8676
|
+
...options
|
|
8677
|
+
});
|
|
8678
|
+
}
|
|
8679
|
+
}
|
|
8680
|
+
class SMS extends Model {
|
|
8681
|
+
constructor(data = {}, options = {}) {
|
|
8682
|
+
super(data, {
|
|
8683
|
+
endpoint: "/api/phonehub/sms",
|
|
8684
|
+
...options
|
|
8685
|
+
});
|
|
8686
|
+
}
|
|
8687
|
+
/**
|
|
8688
|
+
* Send SMS via PhoneHub (Twilio under the hood).
|
|
8689
|
+
* @param {object} params - { to_number, body, from_number?, group?, metadata? }
|
|
8690
|
+
* @returns {Promise<{success: boolean, model?: SMS, data?: object, error?: string, response?: any}>}
|
|
8691
|
+
*/
|
|
8692
|
+
static async send(params = {}) {
|
|
8693
|
+
const url = "/api/phonehub/sms/send";
|
|
8694
|
+
const resp = await rest.POST(url, params);
|
|
8695
|
+
const body = resp?.data ?? resp;
|
|
8696
|
+
const ok = body?.status === true || body?.success === true;
|
|
8697
|
+
if (ok) {
|
|
8698
|
+
const data = body?.data ?? {};
|
|
8699
|
+
const model = new SMS(data, { endpoint: "/api/phonehub/sms" });
|
|
8700
|
+
return { success: true, model, data, response: resp };
|
|
8701
|
+
}
|
|
8702
|
+
return { success: false, error: body?.error || "Failed to send SMS", response: resp };
|
|
8703
|
+
}
|
|
8704
|
+
}
|
|
8705
|
+
class SMSList extends Collection {
|
|
8706
|
+
constructor(options = {}) {
|
|
8707
|
+
super({
|
|
8708
|
+
ModelClass: SMS,
|
|
8709
|
+
endpoint: "/api/phonehub/sms",
|
|
8710
|
+
size: 10,
|
|
8711
|
+
...options
|
|
8712
|
+
});
|
|
8713
|
+
}
|
|
8714
|
+
}
|
|
8715
|
+
class PhoneNumberView extends View {
|
|
8716
|
+
constructor(options = {}) {
|
|
8717
|
+
super({
|
|
8718
|
+
className: "phone-number-view",
|
|
8719
|
+
...options
|
|
8720
|
+
});
|
|
8721
|
+
this.model = options.model || new PhoneNumber(options.data || {});
|
|
8722
|
+
this.template = `
|
|
8723
|
+
<div class="phone-number-view-container">
|
|
8724
|
+
<!-- Header -->
|
|
8725
|
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
8726
|
+
<!-- Left Side: Icon & Info -->
|
|
8727
|
+
<div class="d-flex align-items-center gap-3">
|
|
8728
|
+
<div class="fs-1 text-primary">
|
|
8729
|
+
<i class="bi bi-telephone"></i>
|
|
8730
|
+
</div>
|
|
8731
|
+
<div>
|
|
8732
|
+
<h3 class="mb-1">{{model.phone_number|default('Unknown Number')}}</h3>
|
|
8733
|
+
<div class="text-muted small">
|
|
8734
|
+
{{model.carrier|default('—')}} {{#model.line_type}}· {{model.line_type|capitalize}}{{/model.line_type}}
|
|
8735
|
+
</div>
|
|
8736
|
+
<div class="text-muted small mt-1">
|
|
8737
|
+
{{#model.country_code}}Country: {{model.country_code}}{{/model.country_code}}
|
|
8738
|
+
{{#model.region}} · Region: {{model.region}}{{/model.region}}
|
|
8739
|
+
{{#model.state}} · State: {{model.state}}{{/model.state}}
|
|
8740
|
+
</div>
|
|
8741
|
+
</div>
|
|
8742
|
+
</div>
|
|
8743
|
+
|
|
8744
|
+
<!-- Right Side: Actions -->
|
|
8745
|
+
<div class="d-flex align-items-center gap-4">
|
|
8746
|
+
<div data-container="phone-context-menu"></div>
|
|
8747
|
+
</div>
|
|
8748
|
+
</div>
|
|
8749
|
+
|
|
8750
|
+
<!-- Tabs -->
|
|
8751
|
+
<div data-container="phone-tabs"></div>
|
|
8752
|
+
</div>
|
|
8753
|
+
`;
|
|
8754
|
+
}
|
|
8755
|
+
async onInit() {
|
|
8756
|
+
this.overviewView = new DataView({
|
|
8757
|
+
model: this.model,
|
|
8758
|
+
className: "p-3",
|
|
8759
|
+
showEmptyValues: true,
|
|
8760
|
+
emptyValueText: "—",
|
|
8761
|
+
columns: 2,
|
|
8762
|
+
fields: [
|
|
8763
|
+
{ name: "phone_number", label: "Phone Number", cols: 6 },
|
|
8764
|
+
{ name: "country_code", label: "Country Code", cols: 6 },
|
|
8765
|
+
{ name: "region", label: "Region", cols: 6 },
|
|
8766
|
+
{ name: "state", label: "State", cols: 6 },
|
|
8767
|
+
{ name: "registered_owner", label: "Registered Owner", cols: 6 },
|
|
8768
|
+
{ name: "owner_type", label: "Owner Type", formatter: "capitalize", cols: 6 },
|
|
8769
|
+
{ name: "is_valid", label: "Valid", formatter: "yesnoicon", cols: 4 },
|
|
8770
|
+
{ name: "is_mobile", label: "Mobile", formatter: "yesnoicon", cols: 4 },
|
|
8771
|
+
{ name: "is_voip", label: "VOIP", formatter: "yesnoicon", cols: 4 }
|
|
8772
|
+
]
|
|
8773
|
+
});
|
|
8774
|
+
this.carrierView = new DataView({
|
|
8775
|
+
model: this.model,
|
|
8776
|
+
className: "p-3",
|
|
8777
|
+
showEmptyValues: true,
|
|
8778
|
+
emptyValueText: "—",
|
|
8779
|
+
columns: 2,
|
|
8780
|
+
fields: [
|
|
8781
|
+
{ name: "carrier", label: "Carrier", cols: 6 },
|
|
8782
|
+
{ name: "line_type", label: "Line Type", formatter: "capitalize", cols: 6 },
|
|
8783
|
+
{ name: "lookup_provider", label: "Lookup Provider", formatter: "capitalize", cols: 6 },
|
|
8784
|
+
{ name: "lookup_count", label: "Lookup Count", cols: 6 },
|
|
8785
|
+
{ name: "last_lookup_at", label: "Last Lookup", formatter: "datetime", cols: 6 },
|
|
8786
|
+
{ name: "lookup_expires_at", label: "Cache Expires", formatter: "datetime", cols: 6 }
|
|
8787
|
+
]
|
|
8788
|
+
});
|
|
8789
|
+
this.addressView = new DataView({
|
|
8790
|
+
model: this.model,
|
|
8791
|
+
className: "p-3",
|
|
8792
|
+
showEmptyValues: true,
|
|
8793
|
+
emptyValueText: "—",
|
|
8794
|
+
columns: 2,
|
|
8795
|
+
fields: [
|
|
8796
|
+
{ name: "address_line1", label: "Address Line 1", cols: 12 },
|
|
8797
|
+
{ name: "address_city", label: "City", cols: 4 },
|
|
8798
|
+
{ name: "address_state", label: "State", cols: 4 },
|
|
8799
|
+
{ name: "address_zip", label: "ZIP", cols: 4 },
|
|
8800
|
+
{ name: "address_country", label: "Country", cols: 6 }
|
|
8801
|
+
]
|
|
8802
|
+
});
|
|
8803
|
+
this.metadataView = new DataView({
|
|
8804
|
+
model: this.model,
|
|
8805
|
+
className: "p-3",
|
|
8806
|
+
showEmptyValues: true,
|
|
8807
|
+
emptyValueText: "—",
|
|
8808
|
+
columns: 2,
|
|
8809
|
+
fields: [
|
|
8810
|
+
{ name: "id", label: "Record ID", cols: 6 },
|
|
8811
|
+
{ name: "created", label: "Created", formatter: "datetime", cols: 6 },
|
|
8812
|
+
{ name: "modified", label: "Last Modified", formatter: "datetime", cols: 6 }
|
|
8813
|
+
]
|
|
8814
|
+
});
|
|
8815
|
+
const tabs = {
|
|
8816
|
+
"Overview": this.overviewView,
|
|
8817
|
+
"Carrier": this.carrierView,
|
|
8818
|
+
"Address": this.addressView,
|
|
8819
|
+
"Metadata": this.metadataView
|
|
8820
|
+
};
|
|
8821
|
+
this.tabView = new TabView({
|
|
8822
|
+
containerId: "phone-tabs",
|
|
8823
|
+
tabs,
|
|
8824
|
+
activeTab: "Overview"
|
|
8825
|
+
});
|
|
8826
|
+
this.addChild(this.tabView);
|
|
8827
|
+
const menuItems = [
|
|
8828
|
+
{ label: "Refresh Lookup", action: "refresh-lookup", icon: "bi-arrow-repeat" },
|
|
8829
|
+
{ type: "divider" },
|
|
8830
|
+
{ label: "Delete Record", action: "delete-phone", icon: "bi-trash", danger: true }
|
|
8831
|
+
];
|
|
8832
|
+
const ctxMenu = new ContextMenu({
|
|
8833
|
+
containerId: "phone-context-menu",
|
|
8834
|
+
className: "context-menu-view header-menu-absolute",
|
|
8835
|
+
context: this.model,
|
|
8836
|
+
config: {
|
|
8837
|
+
icon: "bi-three-dots-vertical",
|
|
8838
|
+
items: menuItems
|
|
8839
|
+
}
|
|
8840
|
+
});
|
|
8841
|
+
this.addChild(ctxMenu);
|
|
8842
|
+
}
|
|
8843
|
+
// Actions
|
|
8844
|
+
async onActionRefreshLookup() {
|
|
8845
|
+
const number = this.model.get("phone_number");
|
|
8846
|
+
if (!number) {
|
|
8847
|
+
this.getApp()?.toast?.warning?.("No phone number to lookup");
|
|
8848
|
+
return;
|
|
8849
|
+
}
|
|
8850
|
+
try {
|
|
8851
|
+
this.getApp()?.toast?.info?.("Refreshing lookup...");
|
|
8852
|
+
const resp = await PhoneNumber.lookup(number, { force_refresh: true });
|
|
8853
|
+
if (resp.success && resp.data) {
|
|
8854
|
+
this.model.set(resp.data);
|
|
8855
|
+
await this.render();
|
|
8856
|
+
this.getApp()?.toast?.success?.("Lookup refreshed");
|
|
8857
|
+
} else {
|
|
8858
|
+
const msg = resp.error || "Lookup failed";
|
|
8859
|
+
this.getApp()?.toast?.error?.(msg);
|
|
8860
|
+
}
|
|
8861
|
+
} catch (e) {
|
|
8862
|
+
this.getApp()?.toast?.error?.(e.message || "Lookup failed");
|
|
8863
|
+
}
|
|
8864
|
+
}
|
|
8865
|
+
async onActionDeletePhone() {
|
|
8866
|
+
const confirmed = await Dialog$1.confirm(
|
|
8867
|
+
`Are you sure you want to delete the record for "${this.model.get("phone_number") || "this number"}"?`,
|
|
8868
|
+
"Confirm Deletion",
|
|
8869
|
+
{ confirmClass: "btn-danger", confirmText: "Delete" }
|
|
8870
|
+
);
|
|
8871
|
+
if (!confirmed) return;
|
|
8872
|
+
try {
|
|
8873
|
+
const resp = await this.model.destroy();
|
|
8874
|
+
if (resp?.success) {
|
|
8875
|
+
this.emit("phone:deleted", { model: this.model });
|
|
8876
|
+
} else {
|
|
8877
|
+
this.getApp()?.toast?.error?.("Delete failed");
|
|
8878
|
+
}
|
|
8879
|
+
} catch (e) {
|
|
8880
|
+
this.getApp()?.toast?.error?.(e.message || "Delete failed");
|
|
8881
|
+
}
|
|
8882
|
+
}
|
|
8883
|
+
}
|
|
8884
|
+
PhoneNumberView.MODEL_CLASS = PhoneNumber;
|
|
8885
|
+
class PhoneNumberTablePage extends TablePage {
|
|
8886
|
+
constructor(options = {}) {
|
|
8887
|
+
super({
|
|
8888
|
+
...options,
|
|
8889
|
+
// Identity
|
|
8890
|
+
name: "admin_phonehub_numbers",
|
|
8891
|
+
pageName: "Phone Numbers",
|
|
8892
|
+
router: "admin/phonehub/numbers",
|
|
8893
|
+
// Data source
|
|
8894
|
+
Collection: PhoneNumberList,
|
|
8895
|
+
// Item view configuration
|
|
8896
|
+
itemView: PhoneNumberView,
|
|
8897
|
+
viewDialogOptions: {
|
|
8898
|
+
header: false
|
|
8899
|
+
// size: 'xl'
|
|
8900
|
+
},
|
|
8901
|
+
// Column definitions
|
|
8902
|
+
columns: [
|
|
8903
|
+
{ key: "phone_number", label: "Phone Number", sortable: true },
|
|
8904
|
+
{ key: "carrier", label: "Carrier", sortable: true, formatter: "default('—')" },
|
|
8905
|
+
{ key: "line_type", label: "Line Type", sortable: true, formatter: "capitalize" },
|
|
8906
|
+
{ key: "is_mobile", label: "Mobile", formatter: "yesnoicon" },
|
|
8907
|
+
{ key: "is_voip", label: "VOIP", formatter: "yesnoicon" },
|
|
8908
|
+
{ key: "is_valid", label: "Valid", formatter: "yesnoicon" },
|
|
8909
|
+
{ key: "registered_owner", label: "Owner", sortable: true, formatter: "default('—')" },
|
|
8910
|
+
{ key: "owner_type", label: "Owner Type", formatter: "capitalize" },
|
|
8911
|
+
{ key: "last_lookup_at|relative", label: "Last Lookup", sortable: true }
|
|
8912
|
+
],
|
|
8913
|
+
// Table features
|
|
8914
|
+
selectable: true,
|
|
8915
|
+
searchable: true,
|
|
8916
|
+
sortable: true,
|
|
8917
|
+
filterable: true,
|
|
8918
|
+
paginated: true,
|
|
8919
|
+
// Row action
|
|
8920
|
+
clickAction: "view",
|
|
8921
|
+
// Toolbar
|
|
8922
|
+
showRefresh: true,
|
|
8923
|
+
showAdd: true,
|
|
8924
|
+
showExport: true,
|
|
8925
|
+
// Empty state
|
|
8926
|
+
emptyMessage: "No phone numbers found.",
|
|
8927
|
+
// Table display options
|
|
8928
|
+
tableOptions: {
|
|
8929
|
+
striped: true,
|
|
8930
|
+
bordered: false,
|
|
8931
|
+
hover: true,
|
|
8932
|
+
responsive: false
|
|
8933
|
+
},
|
|
8934
|
+
tableViewOptions: {
|
|
8935
|
+
addButtonLabel: "Lookup",
|
|
8936
|
+
addButtonIcon: "bi-search",
|
|
8937
|
+
onAdd: (evt) => {
|
|
8938
|
+
evt.preventDefault();
|
|
8939
|
+
this.onLookup();
|
|
8940
|
+
}
|
|
8941
|
+
}
|
|
8942
|
+
});
|
|
8943
|
+
}
|
|
8944
|
+
async onLookup() {
|
|
8945
|
+
const data = await this.getApp().showForm({
|
|
8946
|
+
title: "Lookup Phone Number",
|
|
8947
|
+
fields: [
|
|
8948
|
+
{
|
|
8949
|
+
name: "number",
|
|
8950
|
+
type: "text",
|
|
8951
|
+
required: true
|
|
8952
|
+
}
|
|
8953
|
+
]
|
|
8954
|
+
});
|
|
8955
|
+
if (data && data.number) {
|
|
8956
|
+
const resp = await PhoneNumber.lookup(data.number);
|
|
8957
|
+
if (resp.model) {
|
|
8958
|
+
this.tableView._onRowView(resp);
|
|
8959
|
+
}
|
|
8960
|
+
}
|
|
8961
|
+
}
|
|
8962
|
+
}
|
|
8963
|
+
class SMSView extends View {
|
|
8964
|
+
constructor(options = {}) {
|
|
8965
|
+
super({
|
|
8966
|
+
className: "sms-view",
|
|
8967
|
+
...options
|
|
8968
|
+
});
|
|
8969
|
+
this.model = options.model || new SMS(options.data || {});
|
|
8970
|
+
this.template = `
|
|
8971
|
+
<div class="sms-view-container">
|
|
8972
|
+
<!-- Header -->
|
|
8973
|
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
8974
|
+
<!-- Left Side: Icon & Info -->
|
|
8975
|
+
<div class="d-flex align-items-center gap-3">
|
|
8976
|
+
<div class="fs-1 text-primary">
|
|
8977
|
+
<i class="bi bi-chat-dots"></i>
|
|
8978
|
+
</div>
|
|
8979
|
+
<div>
|
|
8980
|
+
<h3 class="mb-1">
|
|
8981
|
+
{{#model.direction}}{{model.direction|capitalize}}{{/model.direction}}
|
|
8982
|
+
{{^model.direction}}Message{{/model.direction}}
|
|
8983
|
+
<small class="text-muted ms-2">
|
|
8984
|
+
{{#model.status}}[{{model.status|capitalize}}]{{/model.status}}
|
|
8985
|
+
</small>
|
|
8986
|
+
</h3>
|
|
8987
|
+
<div class="text-muted small">
|
|
8988
|
+
{{#model.from_number}}From: {{model.from_number}}{{/model.from_number}}
|
|
8989
|
+
{{#model.to_number}} · To: {{model.to_number}}{{/model.to_number}}
|
|
8990
|
+
</div>
|
|
8991
|
+
<div class="text-muted small mt-1">
|
|
8992
|
+
{{#model.provider}}Provider: {{model.provider|capitalize}}{{/model.provider}}
|
|
8993
|
+
{{#model.provider_message_id}} · SID: {{model.provider_message_id}}{{/model.provider_message_id}}
|
|
8994
|
+
</div>
|
|
8995
|
+
</div>
|
|
8996
|
+
</div>
|
|
8997
|
+
|
|
8998
|
+
<!-- Right Side: Actions -->
|
|
8999
|
+
<div class="d-flex align-items-center gap-4">
|
|
9000
|
+
<div data-container="sms-context-menu"></div>
|
|
9001
|
+
</div>
|
|
9002
|
+
</div>
|
|
9003
|
+
|
|
9004
|
+
<!-- Tabs -->
|
|
9005
|
+
<div data-container="sms-tabs"></div>
|
|
9006
|
+
</div>
|
|
9007
|
+
`;
|
|
9008
|
+
}
|
|
9009
|
+
async onInit() {
|
|
9010
|
+
this.messageView = new DataView({
|
|
9011
|
+
model: this.model,
|
|
9012
|
+
className: "p-3",
|
|
9013
|
+
showEmptyValues: true,
|
|
9014
|
+
emptyValueText: "—",
|
|
9015
|
+
columns: 2,
|
|
9016
|
+
fields: [
|
|
9017
|
+
{ name: "direction", label: "Direction", formatter: "capitalize", cols: 4 },
|
|
9018
|
+
{ name: "status", label: "Status", formatter: "capitalize", cols: 4 },
|
|
9019
|
+
{ name: "from_number", label: "From", cols: 6 },
|
|
9020
|
+
{ name: "to_number", label: "To", cols: 6 },
|
|
9021
|
+
{ name: "body", label: "Message Body", cols: 12 }
|
|
9022
|
+
]
|
|
9023
|
+
});
|
|
9024
|
+
this.deliveryView = new DataView({
|
|
9025
|
+
model: this.model,
|
|
9026
|
+
className: "p-3",
|
|
9027
|
+
showEmptyValues: true,
|
|
9028
|
+
emptyValueText: "—",
|
|
9029
|
+
columns: 2,
|
|
9030
|
+
fields: [
|
|
9031
|
+
{ name: "provider", label: "Provider", formatter: "capitalize", cols: 6 },
|
|
9032
|
+
{ name: "provider_message_id", label: "Provider Message ID", cols: 6 },
|
|
9033
|
+
{ name: "sent_at", label: "Sent At", formatter: "datetime", cols: 6 },
|
|
9034
|
+
{ name: "delivered_at", label: "Delivered At", formatter: "datetime", cols: 6 },
|
|
9035
|
+
{ name: "error_code", label: "Error Code", cols: 6 },
|
|
9036
|
+
{ name: "error_message", label: "Error Message", cols: 12 }
|
|
9037
|
+
]
|
|
9038
|
+
});
|
|
9039
|
+
this.metadataView = new DataView({
|
|
9040
|
+
model: this.model,
|
|
9041
|
+
className: "p-3",
|
|
9042
|
+
showEmptyValues: true,
|
|
9043
|
+
emptyValueText: "—",
|
|
9044
|
+
columns: 2,
|
|
9045
|
+
fields: [
|
|
9046
|
+
{ name: "id", label: "Record ID", cols: 6 },
|
|
9047
|
+
{ name: "created", label: "Created", formatter: "datetime", cols: 6 },
|
|
9048
|
+
{ name: "modified", label: "Last Modified", formatter: "datetime", cols: 6 }
|
|
9049
|
+
]
|
|
9050
|
+
});
|
|
9051
|
+
const tabs = {
|
|
9052
|
+
"Message": this.messageView,
|
|
9053
|
+
"Delivery": this.deliveryView,
|
|
9054
|
+
"Metadata": this.metadataView
|
|
9055
|
+
};
|
|
9056
|
+
this.tabView = new TabView({
|
|
9057
|
+
containerId: "sms-tabs",
|
|
9058
|
+
tabs,
|
|
9059
|
+
activeTab: "Message"
|
|
9060
|
+
});
|
|
9061
|
+
this.addChild(this.tabView);
|
|
9062
|
+
const menuItems = [
|
|
9063
|
+
{ label: "Refresh", action: "refresh-sms", icon: "bi-arrow-repeat" },
|
|
9064
|
+
{ type: "divider" },
|
|
9065
|
+
{ label: "Delete Message", action: "delete-sms", icon: "bi-trash", danger: true }
|
|
9066
|
+
];
|
|
9067
|
+
const ctxMenu = new ContextMenu({
|
|
9068
|
+
containerId: "sms-context-menu",
|
|
9069
|
+
className: "context-menu-view header-menu-absolute",
|
|
9070
|
+
context: this.model,
|
|
9071
|
+
config: {
|
|
9072
|
+
icon: "bi-three-dots-vertical",
|
|
9073
|
+
items: menuItems
|
|
9074
|
+
}
|
|
9075
|
+
});
|
|
9076
|
+
this.addChild(ctxMenu);
|
|
9077
|
+
}
|
|
9078
|
+
// Actions
|
|
9079
|
+
async onActionRefreshSms() {
|
|
9080
|
+
try {
|
|
9081
|
+
this.getApp()?.toast?.info?.("Refreshing message...");
|
|
9082
|
+
await this.model.fetch();
|
|
9083
|
+
await this.render();
|
|
9084
|
+
this.getApp()?.toast?.success?.("Message refreshed");
|
|
9085
|
+
} catch (e) {
|
|
9086
|
+
this.getApp()?.toast?.error?.(e.message || "Refresh failed");
|
|
9087
|
+
}
|
|
9088
|
+
}
|
|
9089
|
+
async onActionDeleteSms() {
|
|
9090
|
+
const title = "Confirm Deletion";
|
|
9091
|
+
const msg = `Are you sure you want to delete this message?`;
|
|
9092
|
+
const confirmed = await Dialog$1.confirm(msg, title, {
|
|
9093
|
+
confirmClass: "btn-danger",
|
|
9094
|
+
confirmText: "Delete"
|
|
9095
|
+
});
|
|
9096
|
+
if (!confirmed) return;
|
|
9097
|
+
try {
|
|
9098
|
+
const resp = await this.model.destroy();
|
|
9099
|
+
if (resp?.success) {
|
|
9100
|
+
this.emit("sms:deleted", { model: this.model });
|
|
9101
|
+
} else {
|
|
9102
|
+
this.getApp()?.toast?.error?.("Delete failed");
|
|
9103
|
+
}
|
|
9104
|
+
} catch (e) {
|
|
9105
|
+
this.getApp()?.toast?.error?.(e.message || "Delete failed");
|
|
9106
|
+
}
|
|
9107
|
+
}
|
|
9108
|
+
}
|
|
9109
|
+
SMSView.MODEL_CLASS = SMS;
|
|
9110
|
+
class SMSTablePage extends TablePage {
|
|
9111
|
+
constructor(options = {}) {
|
|
9112
|
+
super({
|
|
9113
|
+
...options,
|
|
9114
|
+
// Identity
|
|
9115
|
+
name: "admin_phonehub_sms",
|
|
9116
|
+
pageName: "SMS Messages",
|
|
9117
|
+
router: "admin/phonehub/sms",
|
|
9118
|
+
// Data source
|
|
9119
|
+
Collection: SMSList,
|
|
9120
|
+
// Item view configuration
|
|
9121
|
+
itemView: SMSView,
|
|
9122
|
+
viewDialogOptions: {
|
|
9123
|
+
header: false,
|
|
9124
|
+
size: "xl"
|
|
9125
|
+
},
|
|
9126
|
+
// Column definitions
|
|
9127
|
+
columns: [
|
|
9128
|
+
{ key: "direction", label: "Direction", sortable: true },
|
|
9129
|
+
{ key: "from_number", label: "From", sortable: true, formatter: "default('—')" },
|
|
9130
|
+
{ key: "to_number", label: "To", sortable: true, formatter: "default('—')" },
|
|
9131
|
+
{ key: "status", label: "Status", sortable: true },
|
|
9132
|
+
{ key: "provider", label: "Provider", sortable: true, formatter: "default('—')" },
|
|
9133
|
+
{ key: "body", label: "Message", formatter: "default('—')" },
|
|
9134
|
+
{ key: "sent_at", label: "Sent At", sortable: true, formatter: "datetime" },
|
|
9135
|
+
{ key: "delivered_at", label: "Delivered At", sortable: true, formatter: "datetime" },
|
|
9136
|
+
{ key: "created", label: "Created", sortable: true, formatter: "datetime" }
|
|
9137
|
+
],
|
|
9138
|
+
// Table features
|
|
9139
|
+
selectable: true,
|
|
9140
|
+
searchable: true,
|
|
9141
|
+
sortable: true,
|
|
9142
|
+
filterable: true,
|
|
9143
|
+
paginated: true,
|
|
9144
|
+
// Row action
|
|
9145
|
+
clickAction: "view",
|
|
9146
|
+
// Toolbar
|
|
9147
|
+
showRefresh: true,
|
|
9148
|
+
showAdd: false,
|
|
9149
|
+
showExport: true,
|
|
9150
|
+
// Empty state
|
|
9151
|
+
emptyMessage: "No SMS messages found.",
|
|
9152
|
+
// Table display options
|
|
9153
|
+
tableOptions: {
|
|
9154
|
+
striped: true,
|
|
9155
|
+
bordered: false,
|
|
9156
|
+
hover: true,
|
|
9157
|
+
responsive: false
|
|
9158
|
+
}
|
|
9159
|
+
});
|
|
9160
|
+
}
|
|
9161
|
+
}
|
|
8477
9162
|
function registerSystemPages(app, addToMenu = true) {
|
|
8478
9163
|
app.registerPage("system/dashboard", AdminDashboardPage, { permissions: ["view_admin"] });
|
|
8479
9164
|
app.registerPage("system/jobs", JobsAdminPage, { permissions: ["view_jobs", "manage_jobs"] });
|
|
@@ -8502,6 +9187,8 @@ function registerSystemPages(app, addToMenu = true) {
|
|
|
8502
9187
|
app.registerPage("system/push/templates", PushTemplateTablePage, { permissions: ["manage_users"] });
|
|
8503
9188
|
app.registerPage("system/push/deliveries", PushDeliveryTablePage, { permissions: ["manage_users"] });
|
|
8504
9189
|
app.registerPage("system/push/devices", PushDeviceTablePage, { permissions: ["manage_users"] });
|
|
9190
|
+
app.registerPage("system/phonehub/numbers", PhoneNumberTablePage, { permissions: ["manage_users"] });
|
|
9191
|
+
app.registerPage("system/phonehub/sms", SMSTablePage, { permissions: ["manage_users"] });
|
|
8505
9192
|
if (addToMenu && app.sidebar && app.sidebar.getMenuConfig) {
|
|
8506
9193
|
const adminMenuConfig = app.sidebar.getMenuConfig("system");
|
|
8507
9194
|
if (adminMenuConfig && adminMenuConfig.items) {
|
|
@@ -8676,6 +9363,26 @@ function registerSystemPages(app, addToMenu = true) {
|
|
|
8676
9363
|
permissions: ["manage_aws"]
|
|
8677
9364
|
}
|
|
8678
9365
|
]
|
|
9366
|
+
},
|
|
9367
|
+
{
|
|
9368
|
+
text: "Phone Hub",
|
|
9369
|
+
route: null,
|
|
9370
|
+
icon: "bi-telephone",
|
|
9371
|
+
permissions: ["manage_users"],
|
|
9372
|
+
children: [
|
|
9373
|
+
{
|
|
9374
|
+
text: "Numbers",
|
|
9375
|
+
route: "?page=system/phonehub/numbers",
|
|
9376
|
+
icon: "bi-collection",
|
|
9377
|
+
permissions: ["manage_users"]
|
|
9378
|
+
},
|
|
9379
|
+
{
|
|
9380
|
+
text: "SMS",
|
|
9381
|
+
route: "?page=system/phonehub/sms",
|
|
9382
|
+
icon: "bi-chat-dots",
|
|
9383
|
+
permissions: ["manage_users"]
|
|
9384
|
+
}
|
|
9385
|
+
]
|
|
8679
9386
|
}
|
|
8680
9387
|
];
|
|
8681
9388
|
adminMenuConfig.items.unshift(...adminMenuItems);
|