Commit 445d8738 by Mac Stephens

Improve shared device management, add mobile cards, and bring BFD live-update…

Improve shared device management, add mobile cards, and bring BFD live-update handling in line with WC
parent 28a1f426
......@@ -37,6 +37,8 @@ type
FSelectProc: TSelectProc;
FLoading: Boolean;
FFirstLoad: Boolean;
FRefreshPending: Boolean;
FPendingWsData: TJSObject;
[async] procedure GetComplaints;
procedure HandleListClick(e: TJSMouseEvent);
procedure ShowHideBusinessRows;
......@@ -57,6 +59,8 @@ procedure TFViewComplaints.WebFormCreate(Sender: TObject);
begin
Document.addEventListener('click', @HandleListClick);
FFirstLoad := True;
FRefreshPending := False;
FPendingWsData := nil;
GetComplaints;
asm
if (!window.showComplaintDetails) {
......@@ -176,12 +180,31 @@ begin
HideSpinner('spinner');
FFirstLoad := False;
end;
if Assigned(FPendingWsData) then
begin
respObj := FPendingWsData;
FPendingWsData := nil;
ApplyWsData(respObj);
end;
if FRefreshPending then
begin
FRefreshPending := False;
GetComplaints;
end;
end;
end;
procedure TFViewComplaints.RefreshData;
begin
Console.Log('Complaints.RefreshData');
if FLoading then
begin
FRefreshPending := True;
Exit;
end;
GetComplaints;
end;
......@@ -190,7 +213,10 @@ var
complaintsCount: Integer;
begin
if FLoading then
begin
FPendingWsData := aRespObj;
Exit;
end;
xdwdsComplaints.Close;
xdwdsComplaints.SetJsonData(aRespObj['data']);
......
......@@ -31,6 +31,9 @@ type
FUnitsLoaded: Boolean;
FComplaintsLoaded: Boolean;
FLoadingPoints: Boolean;
FRefreshPending: Boolean;
FPendingWsUnitMapData: TJSArray;
FPendingWsComplaintMapData: TJSArray;
mapFilters: TMapFilters;
FPendingUnitId: string;
FPendingComplaintId: string;
......@@ -243,33 +246,54 @@ begin
unitsData := nil;
complaintsData := nil;
// --- Fetch Units ---------------------------------------------------------
// Section: Fetch units.
try
resp := await(xdwcMap.RawInvokeAsync('IApiService.GetUnitMap', []));
root := TJSObject(resp.Result);
unitsData := TJSArray(root['data']);
FUnitsLoaded := True;
FUnitsLoaded := Assigned(unitsData);
except
on E: EXDataClientRequestException do
Console.Log('Units XData error: ' + E.ErrorResult.ErrorMessage);
on E: Exception do
Console.Log('Units error: ' + E.Message);
end;
// --- Fetch Complaints ----------------------------------------------------
// Section: Fetch incidents.
try
resp := await(xdwcMap.RawInvokeAsync('IApiService.GetComplaintMap', []));
root := TJSObject(resp.Result);
complaintsData := TJSArray(root['data']);
FComplaintsLoaded := True;
FComplaintsLoaded := Assigned(complaintsData);
except
on E: EXDataClientRequestException do
Console.Log('Complaints XData error: ' + E.ErrorResult.ErrorMessage);
on E: Exception do
Console.Log('Complaints error: ' + E.Message);
end;
// Note: Apply the latest buffered WebSocket snapshot after the HTTP load.
if Assigned(FPendingWsUnitMapData) then
begin
unitsData := FPendingWsUnitMapData;
FPendingWsUnitMapData := nil;
FUnitsLoaded := True;
end;
// --- Place markers (BeginUpdate wraps both so the map redraws once) ------
if Assigned(FPendingWsComplaintMapData) then
begin
complaintsData := FPendingWsComplaintMapData;
FPendingWsComplaintMapData := nil;
FComplaintsLoaded := True;
end;
// Section: Render both marker sets in one update.
lfMap.BeginUpdate;
try
PlaceUnitMarkers(unitsData);
PlaceComplaintMarkers(complaintsData);
if FUnitsLoaded then
PlaceUnitMarkers(unitsData);
if FComplaintsLoaded then
PlaceComplaintMarkers(complaintsData);
finally
lfMap.EndUpdate;
end;
......@@ -283,6 +307,12 @@ begin
if showBusy then
HideSpinner('spinner');
FLoadingPoints := False;
if FRefreshPending then
begin
FRefreshPending := False;
LoadPointsAsync(False);
end;
end;
end;
......@@ -477,8 +507,13 @@ end;
procedure TFViewMap.ApplyWsUnitMapData(aData: TJSArray);
begin
// Skip if the map is still initialising or an HTTP load is in flight.
if (not Assigned(mapFilters)) or FLoadingPoints then
if FLoadingPoints then
begin
FPendingWsUnitMapData := aData;
Exit;
end;
if not Assigned(mapFilters) then
Exit;
lfMap.BeginUpdate;
......@@ -494,7 +529,13 @@ end;
procedure TFViewMap.ApplyWsComplaintMapData(aData: TJSArray);
begin
if (not Assigned(mapFilters)) or FLoadingPoints then
if FLoadingPoints then
begin
FPendingWsComplaintMapData := aData;
Exit;
end;
if not Assigned(mapFilters) then
Exit;
lfMap.BeginUpdate;
......@@ -692,6 +733,13 @@ end;
procedure TFViewMap.RefreshData;
begin
Console.Log('Map.RefreshData');
if FLoadingPoints then
begin
FRefreshPending := True;
Exit;
end;
LoadPointsAsync(False);
end;
......
......@@ -45,6 +45,8 @@ type
private
FLoading: Boolean;
FFirstLoad: Boolean;
FRefreshPending: Boolean;
FPendingWsData: TJSObject;
[async] procedure GetUnits;
procedure HandleListClick(e: TJSMouseEvent);
procedure PrepareUnitData(aRespObj: TJSObject);
......@@ -66,6 +68,8 @@ begin
DMConnection.ApiConnection.Connected := True;
Document.addEventListener('click', @HandleListClick);
FFirstLoad := True;
FRefreshPending := False;
FPendingWsData := nil;
GetUnits;
asm
......@@ -232,12 +236,31 @@ begin
FFirstLoad := False;
FLoading := False;
if Assigned(FPendingWsData) then
begin
respObj := FPendingWsData;
FPendingWsData := nil;
ApplyWsData(respObj);
end;
if FRefreshPending then
begin
FRefreshPending := False;
GetUnits;
end;
end;
end;
procedure TFViewUnits.RefreshData;
begin
Console.Log('Units.RefreshData');
if FLoading then
begin
FRefreshPending := True;
Exit;
end;
GetUnits;
end;
......@@ -246,7 +269,10 @@ var
unitCount: Integer;
begin
if FLoading then
begin
FPendingWsData := aRespObj;
Exit;
end;
PrepareUnitData(aRespObj);
......
unit Auth.Service;
unit Auth.Service;
interface
......@@ -7,10 +7,10 @@ uses
XData.Web.Client;
const
TOKEN_NAME = 'WEBEMIMOBILE_TOKEN';
CREDENTIAL_NAME = 'WEBEMIMOBILE_CREDENTIAL_ID';
KEY_TYPE_NAME = 'WEBEMIMOBILE_KEY_TYPE';
DEVICE_MANAGER_TOKEN_NAME = 'WEBEMIMOBILE_DEVICE_MANAGER_TOKEN';
TOKEN_NAME = 'WCEMIMOBILE_TOKEN';
CREDENTIAL_NAME = 'WCEMIMOBILE_CREDENTIAL_ID';
KEY_TYPE_NAME = 'WCEMIMOBILE_KEY_TYPE';
DEVICE_MANAGER_TOKEN_NAME = 'WCEMIMOBILE_DEVICE_MANAGER_TOKEN';
type
TOnLoginSuccess = reference to procedure;
......@@ -112,7 +112,35 @@ end;
{ TAuthService }
constructor TAuthService.Create;
const
StorageKeys: array[0..2] of string = (TOKEN_NAME, CREDENTIAL_NAME, KEY_TYPE_NAME);
var
i: Integer;
keyName, oldKeyName, storedValue: string;
begin
// Note: Preserve existing WC registrations when renaming the browser storage keys.
for i := Low(StorageKeys) to High(StorageKeys) do
begin
keyName := StorageKeys[i];
oldKeyName := StringReplace(keyName, 'WCEMIMOBILE_', 'WEBEMIMOBILE_', []);
storedValue := window.localStorage.getItem(oldKeyName);
if not isNull(storedValue) then
begin
if isNull(window.localStorage.getItem(keyName)) then
window.localStorage.setItem(keyName, storedValue);
window.localStorage.removeItem(oldKeyName);
end;
end;
oldKeyName := 'WEBEMIMOBILE_DEVICE_MANAGER_TOKEN';
storedValue := window.sessionStorage.getItem(oldKeyName);
if not isNull(storedValue) then
begin
if isNull(window.sessionStorage.getItem(DEVICE_MANAGER_TOKEN_NAME)) then
window.sessionStorage.setItem(DEVICE_MANAGER_TOKEN_NAME, storedValue);
window.sessionStorage.removeItem(oldKeyName);
end;
FClient := TXDataWebClient.Create(nil);
FClient.Connection := DMConnection.AuthConnection;
end;
......
......@@ -31,10 +31,10 @@ type
function DeviceList: TDataSet; virtual; abstract;
function RevokeDevice(RevokedBy: string; CredentialId: string): Int64; virtual; abstract;
function UnrevokeDevice(CredentialId: string): Int64; virtual; abstract;
function DeviceNameExists(DeviceName: string): Boolean; virtual; abstract;
function PendingPhone(PhoneNumber: string): TDataSet; virtual; abstract;
function AddPendingDevice(DeviceName: string; DeviceType: string; PhoneNumber: string): Int64; virtual; abstract;
function DeletePendingDevice(PhoneNumber: string): Int64; virtual; abstract;
function DeleteDevice(CredentialId: string): Int64; virtual; abstract;
function DeleteDevice(DeviceId: Integer): Int64; virtual; abstract;
function ReserveRedeemCode(PhoneNumber: string; out Code: string): Boolean; virtual; abstract;
function UpdateDeviceUsername(Username: string; CredentialId: string): Int64; virtual; abstract;
function ActiveDevice(CredentialId: string): TDataSet; virtual; abstract;
......
......@@ -377,6 +377,21 @@ object OracleDeviceDatabase: TOracleDeviceDatabase
ParamType = ptInput
end>
end
object uqDeviceName: TUniQuery
Connection = ucDevices
SQL.Strings = (
'SELECT ID FROM DEVICE_REGISTRATIONS'
'WHERE UPPER(TRIM(DEVICE_NAME)) = UPPER(:NAME)')
ReadOnly = True
Left = 760
Top = 440
ParamData = <
item
DataType = ftString
Name = 'NAME'
ParamType = ptInput
end>
end
object uqPendingPhone: TUniQuery
Connection = ucDevices
SQL.Strings = (
......@@ -420,36 +435,19 @@ object OracleDeviceDatabase: TOracleDeviceDatabase
ParamType = ptInput
end>
end
object uqDeletePendingDevice: TUniQuery
Connection = ucDevices
SQL.Strings = (
'DELETE'
'FROM DEVICE_REGISTRATIONS'
'WHERE PHONE_NUMBER = :PHONE'
' AND STATUS = ''pending''')
Active = False
Left = 60
Top = 550
ParamData = <
item
DataType = ftString
Name = 'PHONE'
ParamType = ptInput
end>
end
object uqDeleteDevice: TUniQuery
Connection = ucDevices
SQL.Strings = (
'DELETE'
'FROM DEVICE_REGISTRATIONS'
'WHERE CREDENTIAL_ID = :CID')
'WHERE ID = :ID')
Active = False
Left = 250
Top = 550
ParamData = <
item
DataType = ftString
Name = 'CID'
DataType = ftInteger
Name = 'ID'
ParamType = ptInput
end>
end
......
......@@ -28,8 +28,8 @@ type
uqRevokeDevice: TUniQuery;
uqUnrevokeDevice: TUniQuery;
uqPendingPhone: TUniQuery;
uqDeviceName: TUniQuery;
uqAddPendingDevice: TUniQuery;
uqDeletePendingDevice: TUniQuery;
uqDeleteDevice: TUniQuery;
uqReserveRedeemCode: TUniQuery;
uqUpdateDeviceUsername: TUniQuery;
......@@ -57,10 +57,10 @@ type
function DeviceList: TDataSet; override;
function RevokeDevice(RevokedBy: string; CredentialId: string): Int64; override;
function UnrevokeDevice(CredentialId: string): Int64; override;
function DeviceNameExists(DeviceName: string): Boolean; override;
function PendingPhone(PhoneNumber: string): TDataSet; override;
function AddPendingDevice(DeviceName: string; DeviceType: string; PhoneNumber: string): Int64; override;
function DeletePendingDevice(PhoneNumber: string): Int64; override;
function DeleteDevice(CredentialId: string): Int64; override;
function DeleteDevice(DeviceId: Integer): Int64; override;
function ReserveRedeemCode(PhoneNumber: string; out Code: string): Boolean; override;
function UpdateDeviceUsername(Username: string; CredentialId: string): Int64; override;
function ActiveDevice(CredentialId: string): TDataSet; override;
......@@ -242,6 +242,18 @@ begin
Result := uqUnrevokeDevice.RowsAffected;
end;
function TOracleDeviceDatabase.DeviceNameExists(DeviceName: string): Boolean;
begin
uqDeviceName.Close;
uqDeviceName.ParamByName('NAME').AsString := Trim(DeviceName);
uqDeviceName.Open;
try
Result := not uqDeviceName.IsEmpty;
finally
uqDeviceName.Close;
end;
end;
function TOracleDeviceDatabase.PendingPhone(PhoneNumber: string): TDataSet;
begin
uqPendingPhone.Close;
......@@ -260,18 +272,10 @@ begin
Result := uqAddPendingDevice.RowsAffected;
end;
function TOracleDeviceDatabase.DeletePendingDevice(PhoneNumber: string): Int64;
begin
uqDeletePendingDevice.Close;
uqDeletePendingDevice.ParamByName('PHONE').AsString := PhoneNumber;
uqDeletePendingDevice.ExecSQL;
Result := uqDeletePendingDevice.RowsAffected;
end;
function TOracleDeviceDatabase.DeleteDevice(CredentialId: string): Int64;
function TOracleDeviceDatabase.DeleteDevice(DeviceId: Integer): Int64;
begin
uqDeleteDevice.Close;
uqDeleteDevice.ParamByName('CID').AsString := CredentialId;
uqDeleteDevice.ParamByName('ID').AsInteger := DeviceId;
uqDeleteDevice.ExecSQL;
Result := uqDeleteDevice.RowsAffected;
end;
......
......@@ -379,6 +379,21 @@ object PostgreSQLDeviceDatabase: TPostgreSQLDeviceDatabase
Value = nil
end>
end
object uqDeviceName: TUniQuery
Connection = ucDevices
SQL.Strings = (
'SELECT ID FROM lems.device_registrations'
'WHERE UPPER(TRIM(DEVICE_NAME)) = UPPER(:NAME)')
ReadOnly = True
Left = 760
Top = 440
ParamData = <
item
DataType = ftString
Name = 'NAME'
ParamType = ptInput
end>
end
object uqPendingPhone: TUniQuery
Connection = ucDevices
SQL.Strings = (
......@@ -423,32 +438,16 @@ object PostgreSQLDeviceDatabase: TPostgreSQLDeviceDatabase
Value = nil
end>
end
object uqDeletePendingDevice: TUniQuery
Connection = ucDevices
SQL.Strings = (
'DELETE FROM lems.device_registrations WHERE phone_number = :PHON' +
'E AND status = '#39'pending'#39)
Left = 60
Top = 566
ParamData = <
item
DataType = ftString
Name = 'PHONE'
ParamType = ptInput
Value = nil
end>
end
object uqDeleteDevice: TUniQuery
Connection = ucDevices
SQL.Strings = (
'DELETE FROM lems.device_registrations WHERE credential_id = :CID')
'DELETE FROM lems.device_registrations WHERE id = :ID')
Left = 188
Top = 530
ParamData = <
item
DataType = ftString
Name = 'CID'
DataType = ftInteger
Name = 'ID'
ParamType = ptInput
Value = nil
end>
......
......@@ -28,8 +28,8 @@ type
uqRevokeDevice: TUniQuery;
uqUnrevokeDevice: TUniQuery;
uqPendingPhone: TUniQuery;
uqDeviceName: TUniQuery;
uqAddPendingDevice: TUniQuery;
uqDeletePendingDevice: TUniQuery;
uqDeleteDevice: TUniQuery;
uqReserveRedeemCode: TUniQuery;
uqMarkRedeemCodeUsed: TUniQuery;
......@@ -58,10 +58,10 @@ type
function DeviceList: TDataSet; override;
function RevokeDevice(RevokedBy: string; CredentialId: string): Int64; override;
function UnrevokeDevice(CredentialId: string): Int64; override;
function DeviceNameExists(DeviceName: string): Boolean; override;
function PendingPhone(PhoneNumber: string): TDataSet; override;
function AddPendingDevice(DeviceName: string; DeviceType: string; PhoneNumber: string): Int64; override;
function DeletePendingDevice(PhoneNumber: string): Int64; override;
function DeleteDevice(CredentialId: string): Int64; override;
function DeleteDevice(DeviceId: Integer): Int64; override;
function ReserveRedeemCode(PhoneNumber: string; out Code: string): Boolean; override;
function UpdateDeviceUsername(Username: string; CredentialId: string): Int64; override;
function ActiveDevice(CredentialId: string): TDataSet; override;
......@@ -243,6 +243,18 @@ begin
Result := uqUnrevokeDevice.RowsAffected;
end;
function TPostgreSQLDeviceDatabase.DeviceNameExists(DeviceName: string): Boolean;
begin
uqDeviceName.Close;
uqDeviceName.ParamByName('NAME').AsString := Trim(DeviceName);
uqDeviceName.Open;
try
Result := not uqDeviceName.IsEmpty;
finally
uqDeviceName.Close;
end;
end;
function TPostgreSQLDeviceDatabase.PendingPhone(PhoneNumber: string): TDataSet;
begin
uqPendingPhone.Close;
......@@ -261,18 +273,10 @@ begin
Result := uqAddPendingDevice.RowsAffected;
end;
function TPostgreSQLDeviceDatabase.DeletePendingDevice(PhoneNumber: string): Int64;
begin
uqDeletePendingDevice.Close;
uqDeletePendingDevice.ParamByName('PHONE').AsString := PhoneNumber;
uqDeletePendingDevice.ExecSQL;
Result := uqDeletePendingDevice.RowsAffected;
end;
function TPostgreSQLDeviceDatabase.DeleteDevice(CredentialId: string): Int64;
function TPostgreSQLDeviceDatabase.DeleteDevice(DeviceId: Integer): Int64;
begin
uqDeleteDevice.Close;
uqDeleteDevice.ParamByName('CID').AsString := CredentialId;
uqDeleteDevice.ParamByName('ID').AsInteger := DeviceId;
uqDeleteDevice.ExecSQL;
Result := uqDeleteDevice.RowsAffected;
end;
......
......@@ -39,8 +39,7 @@ type
function UnrevokeDevice(const CredentialId: string): TJSONObject;
function AddPendingDevice(const DeviceName, PhoneNumber,
DeviceType: string): TJSONObject;
function DeletePendingDevice(const PhoneNumber: string): TJSONObject;
function DeleteDevice(CredentialId: string): TJSONObject;
function DeleteDevice(DeviceId: Integer): TJSONObject;
function SendAppLink(const PhoneNumber: string): TJSONObject;
function UpdateDeviceUsername(const CredentialId, Username: string): TJSONObject;
end;
......
......@@ -43,8 +43,7 @@ type
function UnrevokeDevice(const CredentialId: string): TJSONObject;
function AddPendingDevice(const DeviceName, PhoneNumber,
DeviceType: string): TJSONObject;
function DeletePendingDevice(const PhoneNumber: string): TJSONObject;
function DeleteDevice(CredentialId: string): TJSONObject;
function DeleteDevice(DeviceId: Integer): TJSONObject;
function SendAppLink(const PhoneNumber: string): TJSONObject;
function UpdateDeviceUsername(const CredentialId, Username: string): TJSONObject;
end;
......@@ -527,6 +526,13 @@ begin
store := CreateDeviceStore;
try
if store.DeviceNameExists(DeviceName) then
begin
Result.AddPair('status', 'error');
Result.AddPair('message', 'A device with that name already exists. Enter a different device name.');
Exit;
end;
// Reject if a pending entry with this phone already exists
q := store.PendingPhone(normalizedPhone);
if not q.IsEmpty then
......@@ -549,50 +555,7 @@ begin
end;
end;
function TApiService.DeletePendingDevice(const PhoneNumber: string): TJSONObject;
var
affectedRows: Int64;
store: TDeviceStore;
normalizedPhone: string;
begin
RequireAdmin;
Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
try
normalizedPhone := NormalizePhoneE164Api(PhoneNumber);
except
on E: Exception do
begin
Result.AddPair('status', 'error');
Result.AddPair('message', E.Message);
Exit;
end;
end;
store := CreateDeviceStore;
try
affectedRows := store.DeletePendingDevice(normalizedPhone);
if affectedRows > 0 then
begin
Logger.Log(2, 'TApiService.DeletePendingDevice - deleted phone: ' + normalizedPhone);
Result.AddPair('status', 'ok');
Result.AddPair('message', 'Pending device removed.');
end
else
begin
Result.AddPair('status', 'error');
Result.AddPair('message', 'Pending device not found.');
end;
finally
store.Free;
end;
end;
function TApiService.DeleteDevice(CredentialId: string): TJSONObject;
function TApiService.DeleteDevice(DeviceId: Integer): TJSONObject;
var
affectedRows: Int64;
store: TDeviceStore;
......@@ -602,21 +565,20 @@ begin
Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
if Trim(CredentialId) = '' then
if DeviceId <= 0 then
begin
Result.AddPair('status', 'error');
Result.AddPair('message', 'CredentialId is required.');
Result.AddPair('message', 'DeviceId is required.');
Exit;
end;
store := CreateDeviceStore;
try
affectedRows := store.DeleteDevice(Trim(CredentialId));
affectedRows := store.DeleteDevice(DeviceId);
if affectedRows > 0 then
begin
Logger.Log(2, 'TApiService.DeleteDevice - deleted credential: ' +
Copy(CredentialId, 1, 20));
Logger.Log(2, 'TApiService.DeleteDevice - deleted id: ' + IntToStr(DeviceId));
Result.AddPair('status', 'ok');
Result.AddPair('message', 'Device deleted.');
end
......
......@@ -81,6 +81,15 @@ object FViewDeviceManager: TFViewDeviceManager
WidthPercent = 100.000000000000000000
OnClick = btnLogoutClick
end
object btnRefresh: TWebButton
Left = 620
Top = 50
Width = 75
Height = 25
Caption = 'Refresh'
ElementID = 'view.devmgr.btnrefresh'
OnClick = btnRefreshClick
end
object XDataWebClient: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 800
......
<div class="container-fluid p-3 h-100 d-flex flex-column">
<div class="d-flex align-items-center mb-3">
<div class="d-flex align-items-center flex-wrap gap-2 mb-3">
<h5 class="mb-0 me-auto">Device Management</h5>
<button id="view.devmgr.btnlogout" class="btn btn-outline-secondary btn-sm me-2" type="button">
Log Out
</button>
<button id="view.devmgr.btnrefresh"
class="btn btn-outline-secondary btn-sm"
onclick="document.dispatchEvent(new CustomEvent('devmgr-refresh'))">
type="button">
Refresh
</button>
</div>
......@@ -26,9 +26,9 @@
<!-- Add pending device -->
<div class="card mb-3">
<div class="card-body py-2">
<div class="d-flex align-items-start gap-2 flex-wrap">
<span class="fw-semibold text-nowrap small mt-2">Add Device:</span>
<div style="width:180px;">
<div class="row g-2 align-items-start">
<div class="col-12 col-md-auto"><span class="fw-semibold text-nowrap small">Add Device:</span></div>
<div class="col-6 col-md-auto">
<input type="text"
id="view.devmgr.newname"
class="form-control form-control-sm"
......@@ -36,7 +36,7 @@
required>
<div class="invalid-feedback small">Device name is required.</div>
</div>
<div style="width:160px;">
<div class="col-6 col-md-auto">
<input type="tel"
id="view.devmgr.newphone"
class="form-control form-control-sm"
......@@ -44,7 +44,7 @@
required>
<div class="invalid-feedback small">Phone number is required.</div>
</div>
<div style="width:140px;">
<div class="col-6 col-md-auto">
<select id="view.devmgr.newtype"
class="form-select form-select-sm"
required>
......@@ -56,20 +56,24 @@
</select>
<div class="invalid-feedback small">Device type is required.</div>
</div>
<button id="view.devmgr.btnadd"
class="btn btn-primary btn-sm">Add</button>
<div class="col-6 col-md-auto d-grid">
<button id="view.devmgr.btnadd" class="btn btn-primary btn-sm">Add</button>
</div>
</div>
</div>
</div>
<!-- Device table -->
<div class="table-responsive flex-grow-1">
<div class="flex-grow-1 overflow-auto">
<div id="view.devmgr.cards" class="d-md-none"></div>
<div class="table-responsive d-none d-md-block">
<table class="table table-sm table-hover align-middle" id="view.devmgr.table">
<thead class="table-light sticky-top">
<tr>
<th style="min-width:130px;">Device Name</th>
<th style="min-width:100px;">Device Type</th>
<th style="min-width:120px;">Phone</th>
<th style="min-width:145px;">Verification Code</th>
<th style="min-width:110px;">Verification Code</th>
<th style="min-width:140px;">Username</th>
<th>Browser / User Agent</th>
<th style="min-width:135px;">Registered</th>
......@@ -82,6 +86,7 @@
<tbody id="view.devmgr.tbody">
</tbody>
</table>
</div>
<p id="view.devmgr.empty" class="text-muted d-none text-center py-4">
No devices found.
</p>
......
......@@ -17,6 +17,8 @@ type
edtNewPhoneNumber: TWebEdit;
btnAddDevice: TWebButton;
btnLogout: TWebButton;
btnRefresh: TWebButton;
procedure btnRefreshClick(Sender: TObject);
procedure btnLogoutClick(Sender: TObject);
procedure WebFormCreate(Sender: TObject);
procedure btnCloseNotificationClick(Sender: TObject);
......@@ -25,17 +27,15 @@ type
procedure ShowNotification(const AMsg: string; AIsError: Boolean = True);
procedure HideNotification;
procedure ClearTable;
procedure AddDeviceRow(const ACredentialId, AName, ADeviceType,
procedure AddDeviceRow(ADeviceId: Integer; const ACredentialId, AName, ADeviceType,
APhoneNumber, AUsername, AUserAgent, ARegisteredAt, AStatus,
AVerificationCode: string;
AVerificationCodeActive: Boolean);
AVerificationCodeActive, AMobile: Boolean);
[async] procedure LoadDevices;
[async] procedure RevokeDevice(const ACredentialId, AName: string);
[async] procedure UnrevokeDevice(const ACredentialId, AName: string);
procedure ConfirmDeleteDevice(ACredentialId, APhoneNumber,
AName: string; AIsPending: Boolean);
[async] procedure DeletePendingDevice(const APhoneNumber, AName: string);
[async] procedure DeleteDevice(ACredentialId, AName: string);
procedure ConfirmDeleteDevice(ADeviceId: Integer; ACredentialId, AName: string);
[async] procedure DeleteDevice(ADeviceId: Integer; AName: string; AIsCurrentDevice: Boolean);
[async] procedure SendAppLink(const APhoneNumber, AName: string);
[async] procedure AddPendingDevice;
[async] procedure UpdateDeviceUsername(const ACredentialId, AUsername: string);
......@@ -49,6 +49,7 @@ implementation
uses
Auth.Service,
Module.Websocket,
Utils;
{$R *.dfm}
......@@ -67,6 +68,12 @@ begin
Result := AE164;
end;
procedure TFViewDeviceManager.btnRefreshClick(Sender: TObject);
begin
HideNotification;
LoadDevices;
end;
procedure TFViewDeviceManager.WebFormCreate(Sender: TObject);
begin
btnLogout.Visible := AuthService.DeviceManagementMode;
......@@ -145,20 +152,39 @@ begin
asm
var tbody = document.getElementById('view.devmgr.tbody');
if (tbody) tbody.innerHTML = '';
var cards = document.getElementById('view.devmgr.cards');
if (cards) cards.innerHTML = '';
var empty = document.getElementById('view.devmgr.empty');
if (empty) empty.classList.add('d-none');
end;
end;
procedure TFViewDeviceManager.AddDeviceRow(const ACredentialId, AName,
procedure TFViewDeviceManager.AddDeviceRow(ADeviceId: Integer; const ACredentialId, AName,
ADeviceType, APhoneNumber, AUsername, AUserAgent, ARegisteredAt, AStatus,
AVerificationCode: string; AVerificationCodeActive: Boolean);
AVerificationCode: string; AVerificationCodeActive, AMobile: Boolean);
var
tbody, tr, tdName, tdPhone, tdCode, tdUser, tdAgent, tdReg, tdStatus,
tbody, tr, tdName, tdType, tdPhone, tdCode, tdUser, tdAgent, tdReg, tdStatus,
tdAccess, tdDelete, tdSend: TJSHTMLElement;
btn, btnSend, inp: TJSHTMLElement;
card, cardBody, fields, details, summary, actions: TJSHTMLElement;
displayDate, displayPhone: string;
isPending, isRevoked, isActive, isIPhone, isAndroid: Boolean;
procedure AddCardField(Parent: TJSHTMLElement; LabelText: string; Cell: TJSHTMLElement);
var
field, caption: TJSHTMLElement;
begin
field := TJSHTMLElement(document.createElement('div'));
field.className := 'mb-2 text-break';
if Parent = fields then field.classList.add('col-6');
caption := TJSHTMLElement(document.createElement('div'));
caption.className := 'small text-muted';
caption.innerText := LabelText;
field.appendChild(caption);
while Assigned(Cell.firstChild) do
field.appendChild(Cell.firstChild);
Parent.appendChild(field);
end;
begin
tbody := TJSHTMLElement(document.getElementById('view.devmgr.tbody'));
if not Assigned(tbody) then Exit;
......@@ -182,6 +208,21 @@ begin
tdName.innerHTML := '<em class="text-muted">unnamed</em>';
tr.appendChild(tdName);
tdType := TJSHTMLElement(document.createElement('td'));
if SameText(ADeviceType, 'iphone') then
tdType.innerText := 'iPhone'
else if SameText(ADeviceType, 'android') then
tdType.innerText := 'Android'
else if SameText(ADeviceType, 'browser') then
tdType.innerText := 'Browser'
else if SameText(ADeviceType, 'other') then
tdType.innerText := 'Other'
else if Trim(ADeviceType) = '' then
tdType.innerText := '—'
else
tdType.innerText := ADeviceType;
tr.appendChild(tdType);
// Phone number
tdPhone := TJSHTMLElement(document.createElement('td'));
tdPhone.innerText := displayPhone;
......@@ -299,7 +340,7 @@ begin
btn.innerText := 'Delete';
btn.addEventListener('click', procedure(Event: TJSMouseEvent)
begin
ConfirmDeleteDevice(ACredentialId, APhoneNumber, AName, isPending);
ConfirmDeleteDevice(ADeviceId, ACredentialId, AName);
end);
tdDelete.appendChild(btn);
tr.appendChild(tdDelete);
......@@ -324,7 +365,47 @@ begin
else
tdSend.innerHTML := '<span class="text-muted">—</span>';
tr.appendChild(tdSend);
tbody.appendChild(tr);
if not AMobile then
tbody.appendChild(tr)
else
begin
card := TJSHTMLElement(document.createElement('div'));
card.className := 'card mb-3';
if isPending then card.classList.add('border-warning');
if isRevoked then card.classList.add('border-secondary');
cardBody := TJSHTMLElement(document.createElement('div'));
cardBody.className := 'card-body p-3';
card.appendChild(cardBody);
fields := TJSHTMLElement(document.createElement('div'));
fields.className := 'row gx-3';
cardBody.appendChild(fields);
AddCardField(fields, 'Device', tdName);
AddCardField(fields, 'Type', tdType);
AddCardField(fields, 'Status', tdStatus);
AddCardField(fields, 'Phone', tdPhone);
AddCardField(fields, 'Username', tdUser);
if isPending then AddCardField(fields, 'Verification code', tdCode);
details := TJSHTMLElement(document.createElement('details'));
details.className := 'mb-3';
summary := TJSHTMLElement(document.createElement('summary'));
summary.className := 'text-primary mb-2';
summary.innerText := 'Details';
details.appendChild(summary);
AddCardField(details, 'Browser / User Agent', tdAgent);
AddCardField(details, 'Registered', tdReg);
cardBody.appendChild(details);
actions := TJSHTMLElement(document.createElement('div'));
actions.className := 'd-flex flex-wrap gap-2';
if Assigned(tdAccess.querySelector('button')) then
actions.appendChild(tdAccess.firstChild);
actions.appendChild(tdDelete.firstChild);
if Assigned(tdSend.querySelector('button')) then
actions.appendChild(tdSend.firstChild);
cardBody.appendChild(actions);
TJSHTMLElement(document.getElementById('view.devmgr.cards')).appendChild(card);
end;
end;
procedure TFViewDeviceManager.LoadDevices;
......@@ -333,7 +414,7 @@ var
list: TJSObject;
data: TJSArray;
item: TJSObject;
i, count: Integer;
i, count, layout: Integer;
begin
ClearTable;
HideNotification;
......@@ -356,7 +437,9 @@ begin
for i := 0 to data.Length - 1 do
begin
item := TJSObject(data[i]);
AddDeviceRow(
for layout := 0 to 1 do
AddDeviceRow(
Integer(item['id']),
string(item['credential_id']),
string(item['device_name']),
string(item['device_type']),
......@@ -366,7 +449,8 @@ begin
string(item['registered_at']),
string(item['status']),
string(item['verification_code']),
JS.toBoolean(item['verification_code_active'])
JS.toBoolean(item['verification_code_active']),
layout = 1
);
end;
......@@ -400,62 +484,52 @@ begin
end;
end;
procedure TFViewDeviceManager.ConfirmDeleteDevice(ACredentialId,
APhoneNumber, AName: string; AIsPending: Boolean);
procedure TFViewDeviceManager.ConfirmDeleteDevice(ADeviceId: Integer; ACredentialId, AName: string);
var
isCurrentDevice: Boolean;
messageText: string;
begin
isCurrentDevice := (ACredentialId <> '') and (ACredentialId = AuthService.GetCredentialId);
messageText := 'Are you sure you want to permanently delete device "' + AName +
'"? This cannot be undone.';
if isCurrentDevice then
messageText := messageText + ' This is the browser you are using. You will be signed out and must register it again to use emiMobile.';
ShowConfirmationModal(
'Are you sure you want to permanently delete device "' + AName +
'"? This cannot be undone.',
messageText,
'Cancel', 'Delete',
procedure(Confirmed: Boolean)
begin
if not Confirmed then
Exit;
if AIsPending then
DeletePendingDevice(APhoneNumber, AName)
else
DeleteDevice(ACredentialId, AName);
DeleteDevice(ADeviceId, AName, isCurrentDevice);
end);
end;
procedure TFViewDeviceManager.DeletePendingDevice(const APhoneNumber, AName: string);
procedure TFViewDeviceManager.DeleteDevice(ADeviceId: Integer; AName: string; AIsCurrentDevice: Boolean);
var
resp: TXDataClientResponse;
res: TJSObject;
status: string;
begin
try
resp := await(XDataWebClient.RawInvokeAsync('IApiService.DeletePendingDevice', [APhoneNumber]));
resp := await(XDataWebClient.RawInvokeAsync('IApiService.DeleteDevice', [ADeviceId]));
res := TJSObject(resp.Result);
status := string(res['status']);
if status = 'ok' then
begin
ShowNotification('Pending device "' + AName + '" removed.', False);
LoadDevices;
end
else
ShowNotification(string(res['message']));
except
on E: Exception do
ShowNotification('Delete failed: ' + E.Message);
end;
end;
procedure TFViewDeviceManager.DeleteDevice(ACredentialId, AName: string);
var
resp: TXDataClientResponse;
res: TJSObject;
status: string;
begin
try
resp := await(XDataWebClient.RawInvokeAsync('IApiService.DeleteDevice', [ACredentialId]));
res := TJSObject(resp.Result);
status := string(res['status']);
if AIsCurrentDevice then
begin
if Assigned(dmWebsocket) then
dmWebsocket.Stop;
AuthService.ClearCredentialId;
AuthService.Logout;
window.location.reload(False);
Exit;
end;
if status = 'ok' then
begin
ShowNotification('Device "' + AName + '" has been deleted.', False);
LoadDevices;
end
......
......@@ -2,7 +2,7 @@
Client=WC
[Settings]
LogFileNum=180
LogFileNum=184
webClientVersion=0.9.4.9
MemoLogLevel=3
FileLogLevel=4
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment