Commit 7fa82e6d by Mac Stephens

Fix WC device management, SMS verification handling, and map marker loading

parent 13ffaf77
...@@ -28,3 +28,15 @@ emiMobileServer/Source/__recovery/ ...@@ -28,3 +28,15 @@ emiMobileServer/Source/__recovery/
*.dll *.dll
webEMIMobile/config/__history/ webEMIMobile/config/__history/
emiMobileServer/Clients/BFD/Web/__history/
emiMobileServer/Clients/WC/Web/__history/
emiMobileServer/Shared/Server/__history/
emiMobileServer/Clients/WC/Web/css/__history/
emiMobileServer/Clients/BFD/Web/Win32/Debug/
emiMobileServer/Clients/WC/Web/Win32/Debug/
...@@ -39,8 +39,7 @@ type ...@@ -39,8 +39,7 @@ type
function UnrevokeDevice(const CredentialId: string): TJSONObject; function UnrevokeDevice(const CredentialId: string): TJSONObject;
function AddPendingDevice(const DeviceName, PhoneNumber, function AddPendingDevice(const DeviceName, PhoneNumber,
DeviceType: string): TJSONObject; DeviceType: string): TJSONObject;
function DeletePendingDevice(const PhoneNumber: string): TJSONObject; function DeleteDevice(DeviceId: Integer): TJSONObject;
function DeleteDevice(CredentialId: string): TJSONObject;
function SendAppLink(const PhoneNumber: string): TJSONObject; function SendAppLink(const PhoneNumber: string): TJSONObject;
function UpdateDeviceUsername(const CredentialId, Username: string): TJSONObject; function UpdateDeviceUsername(const CredentialId, Username: string): TJSONObject;
end; end;
......
...@@ -45,8 +45,7 @@ type ...@@ -45,8 +45,7 @@ type
function UnrevokeDevice(const CredentialId: string): TJSONObject; function UnrevokeDevice(const CredentialId: string): TJSONObject;
function AddPendingDevice(const DeviceName, PhoneNumber, function AddPendingDevice(const DeviceName, PhoneNumber,
DeviceType: string): TJSONObject; DeviceType: string): TJSONObject;
function DeletePendingDevice(const PhoneNumber: string): TJSONObject; function DeleteDevice(DeviceId: Integer): TJSONObject;
function DeleteDevice(CredentialId: string): TJSONObject;
function SendAppLink(const PhoneNumber: string): TJSONObject; function SendAppLink(const PhoneNumber: string): TJSONObject;
function UpdateDeviceUsername(const CredentialId, Username: string): TJSONObject; function UpdateDeviceUsername(const CredentialId, Username: string): TJSONObject;
end; end;
...@@ -1626,6 +1625,21 @@ begin ...@@ -1626,6 +1625,21 @@ begin
try try
q.Connection := conn; q.Connection := conn;
// Note: Device names are unique regardless of capitalization or surrounding spaces.
q.SQL.Text :=
'SELECT id FROM lems.device_registrations ' +
'WHERE UPPER(TRIM(device_name)) = UPPER(:NAME)';
q.ParamByName('NAME').AsString := Trim(DeviceName);
q.Open;
if not q.IsEmpty then
begin
q.Close;
Result.AddPair('status', 'error');
Result.AddPair('message', 'A device with that name already exists. Enter a different device name.');
Exit;
end;
q.Close;
// Reject if a pending entry with this phone already exists // Reject if a pending entry with this phone already exists
q.SQL.Text := q.SQL.Text :=
'SELECT id FROM lems.device_registrations ' + 'SELECT id FROM lems.device_registrations ' +
...@@ -1661,72 +1675,20 @@ begin ...@@ -1661,72 +1675,20 @@ begin
end; end;
end; end;
function TApiService.DeletePendingDevice(const PhoneNumber: string): TJSONObject; function TApiService.DeleteDevice(DeviceId: Integer): TJSONObject;
var var
conn: TUniConnection; conn: TUniConnection;
q: TUniQuery; q: TUniQuery;
normalizedPhone: string;
begin begin
RequireAdmin; RequireAdmin;
Result := TJSONObject.Create; Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result); TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
try if DeviceId <= 0 then
normalizedPhone := NormalizePhoneE164Api(PhoneNumber);
except
on E: Exception do
begin
Result.AddPair('status', 'error');
Result.AddPair('message', E.Message);
Exit;
end;
end;
conn := OpenLemsConnection;
try
q := TUniQuery.Create(nil);
try
q.Connection := conn;
q.SQL.Text :=
'DELETE FROM lems.device_registrations ' +
'WHERE phone_number = :PHONE AND status = ''pending''';
q.ParamByName('PHONE').AsString := normalizedPhone;
q.ExecSQL;
if q.RowsAffected > 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
q.Free;
end;
finally
conn.Free;
end;
end;
function TApiService.DeleteDevice(CredentialId: string): TJSONObject;
var
conn: TUniConnection;
q: TUniQuery;
begin
RequireAdmin;
Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
if Trim(CredentialId) = '' then
begin begin
Result.AddPair('status', 'error'); Result.AddPair('status', 'error');
Result.AddPair('message', 'CredentialId is required.'); Result.AddPair('message', 'DeviceId is required.');
Exit; Exit;
end; end;
...@@ -1737,14 +1699,13 @@ begin ...@@ -1737,14 +1699,13 @@ begin
q.Connection := conn; q.Connection := conn;
q.SQL.Text := q.SQL.Text :=
'DELETE FROM lems.device_registrations ' + 'DELETE FROM lems.device_registrations ' +
'WHERE credential_id = :CID'; 'WHERE id = :ID';
q.ParamByName('CID').AsString := Trim(CredentialId); q.ParamByName('ID').AsInteger := DeviceId;
q.ExecSQL; q.ExecSQL;
if q.RowsAffected > 0 then if q.RowsAffected > 0 then
begin begin
Logger.Log(2, 'TApiService.DeleteDevice - deleted credential: ' + Logger.Log(2, 'TApiService.DeleteDevice - deleted id: ' + IntToStr(DeviceId));
Copy(CredentialId, 1, 20));
Result.AddPair('status', 'ok'); Result.AddPair('status', 'ok');
Result.AddPair('message', 'Device deleted.'); Result.AddPair('message', 'Device deleted.');
end end
......
...@@ -29,7 +29,7 @@ type ...@@ -29,7 +29,7 @@ type
function LoadUserByName(const User, Agency: string): Boolean; function LoadUserByName(const User, Agency: string): Boolean;
function Decrypt(inStr, keyStr: AnsiString): AnsiString; function Decrypt(inStr, keyStr: AnsiString): AnsiString;
function GenerateVerificationCode: string; function GenerateVerificationCode: string;
procedure SendVerificationText(const PhoneNumber, Code: string); function SendVerificationText(const PhoneNumber, Code: string): Boolean;
// Returns True and sets AChallengeB64 if token is valid and of the expected type // Returns True and sets AChallengeB64 if token is valid and of the expected type
function VerifyChallengeToken(const ChallengeToken, ExpectedType: string; function VerifyChallengeToken(const ChallengeToken, ExpectedType: string;
out AChallengeB64: string): Boolean; out AChallengeB64: string): Boolean;
...@@ -209,7 +209,7 @@ begin ...@@ -209,7 +209,7 @@ begin
Result := FormatFloat('000000', value mod 1000000); Result := FormatFloat('000000', value mod 1000000);
end; end;
procedure TAuthService.SendVerificationText(const PhoneNumber, Code: string); function TAuthService.SendVerificationText(const PhoneNumber, Code: string): Boolean;
var var
httpClient: THTTPClient; httpClient: THTTPClient;
bodyStream: TStringStream; bodyStream: TStringStream;
...@@ -239,9 +239,10 @@ begin ...@@ -239,9 +239,10 @@ begin
response := httpClient.Post( response := httpClient.Post(
'https://api.twilio.com/2010-04-01/Accounts/' + 'https://api.twilio.com/2010-04-01/Accounts/' +
ServerConfig.twilioAccountSid + '/Messages.json', bodyStream); ServerConfig.twilioAccountSid + '/Messages.json', bodyStream);
if response.StatusCode >= 300 then Result := (response.StatusCode >= 200) and (response.StatusCode < 300);
raise Exception.CreateFmt('Twilio error %d: %s', if not Result then
[response.StatusCode, response.ContentAsString]); Logger.Log(2, Format('Twilio rejected verification SMS, HTTP %d: %s',
[response.StatusCode, response.ContentAsString]));
finally finally
bodyStream.Free; bodyStream.Free;
end; end;
...@@ -308,6 +309,24 @@ begin ...@@ -308,6 +309,24 @@ begin
end; end;
code := GenerateVerificationCode; code := GenerateVerificationCode;
try
if not SendVerificationText(normalizedPhone, code) then
begin
Result.AddPair('status', 'error');
Result.AddPair('message', 'The SMS provider rejected the verification message. Contact your administrator to check the phone number and SMS configuration.');
Exit;
end;
except
on E: Exception do
begin
Logger.Log(2, '--Error sending registration verification code: ' + E.Message);
Result.AddPair('status', 'error');
Result.AddPair('message', 'The verification code could not be sent. Please try again.');
Exit;
end;
end;
// Note: Persist the code only after the SMS provider accepts the message.
q := TUniQuery.Create(nil); q := TUniQuery.Create(nil);
try try
...@@ -330,33 +349,6 @@ begin ...@@ -330,33 +349,6 @@ begin
q.Free; q.Free;
end; end;
try
SendVerificationText(normalizedPhone, code);
except
on E: Exception do
begin
q := TUniQuery.Create(nil);
try
q.Connection := authDB.ucLemsOCSO;
q.SQL.Text :=
'UPDATE lems.device_registrations ' +
'SET verification_code = NULL, verification_expires_at = NULL ' +
'WHERE phone_number = :PHONE AND status = ''pending'' ' +
'AND verification_code = :CODE';
q.ParamByName('PHONE').AsString := normalizedPhone;
q.ParamByName('CODE').AsString := code;
q.ExecSQL;
finally
q.Free;
end;
Logger.Log(2, '--Error sending registration verification code: ' + E.Message);
Result.AddPair('status', 'error');
Result.AddPair('message', 'The verification code could not be sent. Please try again.');
Exit;
end;
end;
Logger.Log(3, Format( Logger.Log(3, Format(
'Verification code sent for device id %d and device name: %s', 'Verification code sent for device id %d and device name: %s',
[deviceId, deviceName])); [deviceId, deviceName]));
......
...@@ -79,12 +79,16 @@ end; ...@@ -79,12 +79,16 @@ end;
destructor TWsDataModel.Destroy; destructor TWsDataModel.Destroy;
begin begin
FListenerTimer.Enabled := False; if Assigned(FListenerTimer) then
FListenerTimer.Enabled := False;
FListenerTimer.Free; FListenerTimer.Free;
FThread.Terminate; if Assigned(FThread) then
FStopEvent.SetEvent; begin
FThread.WaitFor; FThread.Terminate;
FThread.Free; FStopEvent.SetEvent;
FThread.WaitFor;
FThread.Free;
end;
FListenerDb.Free; FListenerDb.Free;
FStopEvent.Free; FStopEvent.Free;
inherited; inherited;
......
[Settings] [Settings]
LogFileNum=154 LogFileNum=155
webClientVersion=0.9.4.9 webClientVersion=0.9.4.9
MemoLogLevel=3 MemoLogLevel=3
FileLogLevel=4 FileLogLevel=4
......
...@@ -81,6 +81,15 @@ object FViewDeviceManager: TFViewDeviceManager ...@@ -81,6 +81,15 @@ object FViewDeviceManager: TFViewDeviceManager
WidthPercent = 100.000000000000000000 WidthPercent = 100.000000000000000000
OnClick = btnLogoutClick OnClick = btnLogoutClick
end end
object btnRefresh: TWebButton
Left = 620
Top = 50
Width = 75
Height = 25
Caption = 'Refresh'
ElementID = 'view.devmgr.btnrefresh'
OnClick = btnRefreshClick
end
object XDataWebClient: TXDataWebClient object XDataWebClient: TXDataWebClient
Connection = DMConnection.ApiConnection Connection = DMConnection.ApiConnection
Left = 800 Left = 800
......
<div class="container-fluid p-3 h-100 d-flex flex-column"> <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> <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"> <button id="view.devmgr.btnlogout" class="btn btn-outline-secondary btn-sm me-2" type="button">
Log Out Log Out
</button> </button>
<button id="view.devmgr.btnrefresh" <button id="view.devmgr.btnrefresh"
class="btn btn-outline-secondary btn-sm" class="btn btn-outline-secondary btn-sm"
onclick="document.dispatchEvent(new CustomEvent('devmgr-refresh'))"> type="button">
Refresh Refresh
</button> </button>
</div> </div>
...@@ -26,9 +26,9 @@ ...@@ -26,9 +26,9 @@
<!-- Add pending device --> <!-- Add pending device -->
<div class="card mb-3"> <div class="card mb-3">
<div class="card-body py-2"> <div class="card-body py-2">
<div class="d-flex align-items-start gap-2 flex-wrap"> <div class="row g-2 align-items-start">
<span class="fw-semibold text-nowrap small mt-2">Add Device:</span> <div class="col-12 col-md-auto"><span class="fw-semibold text-nowrap small">Add Device:</span></div>
<div style="width:180px;"> <div class="col-6 col-md-auto">
<input type="text" <input type="text"
id="view.devmgr.newname" id="view.devmgr.newname"
class="form-control form-control-sm" class="form-control form-control-sm"
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
required> required>
<div class="invalid-feedback small">Device name is required.</div> <div class="invalid-feedback small">Device name is required.</div>
</div> </div>
<div style="width:160px;"> <div class="col-6 col-md-auto">
<input type="tel" <input type="tel"
id="view.devmgr.newphone" id="view.devmgr.newphone"
class="form-control form-control-sm" class="form-control form-control-sm"
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
required> required>
<div class="invalid-feedback small">Phone number is required.</div> <div class="invalid-feedback small">Phone number is required.</div>
</div> </div>
<div style="width:140px;"> <div class="col-6 col-md-auto">
<select id="view.devmgr.newtype" <select id="view.devmgr.newtype"
class="form-select form-select-sm" class="form-select form-select-sm"
required> required>
...@@ -56,20 +56,24 @@ ...@@ -56,20 +56,24 @@
</select> </select>
<div class="invalid-feedback small">Device type is required.</div> <div class="invalid-feedback small">Device type is required.</div>
</div> </div>
<button id="view.devmgr.btnadd" <div class="col-6 col-md-auto d-grid">
class="btn btn-primary btn-sm">Add</button> <button id="view.devmgr.btnadd" class="btn btn-primary btn-sm">Add</button>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Device table --> <!-- 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"> <table class="table table-sm table-hover align-middle" id="view.devmgr.table">
<thead class="table-light sticky-top"> <thead class="table-light sticky-top">
<tr> <tr>
<th style="min-width:130px;">Device Name</th> <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: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 style="min-width:140px;">Username</th>
<th>Browser / User Agent</th> <th>Browser / User Agent</th>
<th style="min-width:135px;">Registered</th> <th style="min-width:135px;">Registered</th>
...@@ -82,6 +86,7 @@ ...@@ -82,6 +86,7 @@
<tbody id="view.devmgr.tbody"> <tbody id="view.devmgr.tbody">
</tbody> </tbody>
</table> </table>
</div>
<p id="view.devmgr.empty" class="text-muted d-none text-center py-4"> <p id="view.devmgr.empty" class="text-muted d-none text-center py-4">
No devices found. No devices found.
</p> </p>
......
...@@ -17,6 +17,8 @@ type ...@@ -17,6 +17,8 @@ type
edtNewPhoneNumber: TWebEdit; edtNewPhoneNumber: TWebEdit;
btnAddDevice: TWebButton; btnAddDevice: TWebButton;
btnLogout: TWebButton; btnLogout: TWebButton;
btnRefresh: TWebButton;
procedure btnRefreshClick(Sender: TObject);
procedure btnLogoutClick(Sender: TObject); procedure btnLogoutClick(Sender: TObject);
procedure WebFormCreate(Sender: TObject); procedure WebFormCreate(Sender: TObject);
procedure btnCloseNotificationClick(Sender: TObject); procedure btnCloseNotificationClick(Sender: TObject);
...@@ -25,17 +27,15 @@ type ...@@ -25,17 +27,15 @@ type
procedure ShowNotification(const AMsg: string; AIsError: Boolean = True); procedure ShowNotification(const AMsg: string; AIsError: Boolean = True);
procedure HideNotification; procedure HideNotification;
procedure ClearTable; procedure ClearTable;
procedure AddDeviceRow(const ACredentialId, AName, ADeviceType, procedure AddDeviceRow(ADeviceId: Integer; const ACredentialId, AName, ADeviceType,
APhoneNumber, AUsername, AUserAgent, ARegisteredAt, AStatus, APhoneNumber, AUsername, AUserAgent, ARegisteredAt, AStatus,
AVerificationCode: string; AVerificationCode: string;
AVerificationCodeActive: Boolean); AVerificationCodeActive, AMobile: Boolean);
[async] procedure LoadDevices; [async] procedure LoadDevices;
[async] procedure RevokeDevice(const ACredentialId, AName: string); [async] procedure RevokeDevice(const ACredentialId, AName: string);
[async] procedure UnrevokeDevice(const ACredentialId, AName: string); [async] procedure UnrevokeDevice(const ACredentialId, AName: string);
procedure ConfirmDeleteDevice(ACredentialId, APhoneNumber, procedure ConfirmDeleteDevice(ADeviceId: Integer; ACredentialId, AName: string);
AName: string; AIsPending: Boolean); [async] procedure DeleteDevice(ADeviceId: Integer; AName: string; AIsCurrentDevice: Boolean);
[async] procedure DeletePendingDevice(const APhoneNumber, AName: string);
[async] procedure DeleteDevice(ACredentialId, AName: string);
[async] procedure SendAppLink(const APhoneNumber, AName: string); [async] procedure SendAppLink(const APhoneNumber, AName: string);
[async] procedure AddPendingDevice; [async] procedure AddPendingDevice;
[async] procedure UpdateDeviceUsername(const ACredentialId, AUsername: string); [async] procedure UpdateDeviceUsername(const ACredentialId, AUsername: string);
...@@ -49,6 +49,7 @@ implementation ...@@ -49,6 +49,7 @@ implementation
uses uses
Auth.Service, Auth.Service,
Module.Websocket,
Utils; Utils;
{$R *.dfm} {$R *.dfm}
...@@ -67,6 +68,12 @@ begin ...@@ -67,6 +68,12 @@ begin
Result := AE164; Result := AE164;
end; end;
procedure TFViewDeviceManager.btnRefreshClick(Sender: TObject);
begin
HideNotification;
LoadDevices;
end;
procedure TFViewDeviceManager.WebFormCreate(Sender: TObject); procedure TFViewDeviceManager.WebFormCreate(Sender: TObject);
begin begin
btnLogout.Visible := AuthService.DeviceManagementMode; btnLogout.Visible := AuthService.DeviceManagementMode;
...@@ -145,20 +152,39 @@ begin ...@@ -145,20 +152,39 @@ begin
asm asm
var tbody = document.getElementById('view.devmgr.tbody'); var tbody = document.getElementById('view.devmgr.tbody');
if (tbody) tbody.innerHTML = ''; if (tbody) tbody.innerHTML = '';
var cards = document.getElementById('view.devmgr.cards');
if (cards) cards.innerHTML = '';
var empty = document.getElementById('view.devmgr.empty'); var empty = document.getElementById('view.devmgr.empty');
if (empty) empty.classList.add('d-none'); if (empty) empty.classList.add('d-none');
end; end;
end; end;
procedure TFViewDeviceManager.AddDeviceRow(const ACredentialId, AName, procedure TFViewDeviceManager.AddDeviceRow(ADeviceId: Integer; const ACredentialId, AName,
ADeviceType, APhoneNumber, AUsername, AUserAgent, ARegisteredAt, AStatus, ADeviceType, APhoneNumber, AUsername, AUserAgent, ARegisteredAt, AStatus,
AVerificationCode: string; AVerificationCodeActive: Boolean); AVerificationCode: string; AVerificationCodeActive, AMobile: Boolean);
var var
tbody, tr, tdName, tdPhone, tdCode, tdUser, tdAgent, tdReg, tdStatus, tbody, tr, tdName, tdType, tdPhone, tdCode, tdUser, tdAgent, tdReg, tdStatus,
tdAccess, tdDelete, tdSend: TJSHTMLElement; tdAccess, tdDelete, tdSend: TJSHTMLElement;
btn, btnSend, inp: TJSHTMLElement; btn, btnSend, inp: TJSHTMLElement;
card, cardBody, fields, details, summary, actions: TJSHTMLElement;
displayDate, displayPhone: string; displayDate, displayPhone: string;
isPending, isRevoked, isActive, isIPhone, isAndroid: Boolean; 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 begin
tbody := TJSHTMLElement(document.getElementById('view.devmgr.tbody')); tbody := TJSHTMLElement(document.getElementById('view.devmgr.tbody'));
if not Assigned(tbody) then Exit; if not Assigned(tbody) then Exit;
...@@ -182,6 +208,21 @@ begin ...@@ -182,6 +208,21 @@ begin
tdName.innerHTML := '<em class="text-muted">unnamed</em>'; tdName.innerHTML := '<em class="text-muted">unnamed</em>';
tr.appendChild(tdName); 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 // Phone number
tdPhone := TJSHTMLElement(document.createElement('td')); tdPhone := TJSHTMLElement(document.createElement('td'));
tdPhone.innerText := displayPhone; tdPhone.innerText := displayPhone;
...@@ -299,7 +340,7 @@ begin ...@@ -299,7 +340,7 @@ begin
btn.innerText := 'Delete'; btn.innerText := 'Delete';
btn.addEventListener('click', procedure(Event: TJSMouseEvent) btn.addEventListener('click', procedure(Event: TJSMouseEvent)
begin begin
ConfirmDeleteDevice(ACredentialId, APhoneNumber, AName, isPending); ConfirmDeleteDevice(ADeviceId, ACredentialId, AName);
end); end);
tdDelete.appendChild(btn); tdDelete.appendChild(btn);
tr.appendChild(tdDelete); tr.appendChild(tdDelete);
...@@ -324,7 +365,47 @@ begin ...@@ -324,7 +365,47 @@ begin
else else
tdSend.innerHTML := '<span class="text-muted">—</span>'; tdSend.innerHTML := '<span class="text-muted">—</span>';
tr.appendChild(tdSend); 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; end;
procedure TFViewDeviceManager.LoadDevices; procedure TFViewDeviceManager.LoadDevices;
...@@ -333,7 +414,7 @@ var ...@@ -333,7 +414,7 @@ var
list: TJSObject; list: TJSObject;
data: TJSArray; data: TJSArray;
item: TJSObject; item: TJSObject;
i, count: Integer; i, count, layout: Integer;
begin begin
ClearTable; ClearTable;
HideNotification; HideNotification;
...@@ -356,7 +437,9 @@ begin ...@@ -356,7 +437,9 @@ begin
for i := 0 to data.Length - 1 do for i := 0 to data.Length - 1 do
begin begin
item := TJSObject(data[i]); item := TJSObject(data[i]);
AddDeviceRow( for layout := 0 to 1 do
AddDeviceRow(
Integer(item['id']),
string(item['credential_id']), string(item['credential_id']),
string(item['device_name']), string(item['device_name']),
string(item['device_type']), string(item['device_type']),
...@@ -366,7 +449,8 @@ begin ...@@ -366,7 +449,8 @@ begin
string(item['registered_at']), string(item['registered_at']),
string(item['status']), string(item['status']),
string(item['verification_code']), string(item['verification_code']),
JS.toBoolean(item['verification_code_active']) JS.toBoolean(item['verification_code_active']),
layout = 1
); );
end; end;
...@@ -400,62 +484,52 @@ begin ...@@ -400,62 +484,52 @@ begin
end; end;
end; end;
procedure TFViewDeviceManager.ConfirmDeleteDevice(ACredentialId, procedure TFViewDeviceManager.ConfirmDeleteDevice(ADeviceId: Integer; ACredentialId, AName: string);
APhoneNumber, AName: string; AIsPending: Boolean); var
isCurrentDevice: Boolean;
messageText: string;
begin 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( ShowConfirmationModal(
'Are you sure you want to permanently delete device "' + AName + messageText,
'"? This cannot be undone.',
'Cancel', 'Delete', 'Cancel', 'Delete',
procedure(Confirmed: Boolean) procedure(Confirmed: Boolean)
begin begin
if not Confirmed then if not Confirmed then
Exit; Exit;
if AIsPending then DeleteDevice(ADeviceId, AName, isCurrentDevice);
DeletePendingDevice(APhoneNumber, AName)
else
DeleteDevice(ACredentialId, AName);
end); end);
end; end;
procedure TFViewDeviceManager.DeletePendingDevice(const APhoneNumber, AName: string); procedure TFViewDeviceManager.DeleteDevice(ADeviceId: Integer; AName: string; AIsCurrentDevice: Boolean);
var var
resp: TXDataClientResponse; resp: TXDataClientResponse;
res: TJSObject; res: TJSObject;
status: string; status: string;
begin begin
try try
resp := await(XDataWebClient.RawInvokeAsync('IApiService.DeletePendingDevice', [APhoneNumber])); resp := await(XDataWebClient.RawInvokeAsync('IApiService.DeleteDevice', [ADeviceId]));
res := TJSObject(resp.Result); res := TJSObject(resp.Result);
status := string(res['status']); status := string(res['status']);
if status = 'ok' then if status = 'ok' then
begin begin
ShowNotification('Pending device "' + AName + '" removed.', False); if AIsCurrentDevice then
LoadDevices; begin
end if Assigned(dmWebsocket) then
else dmWebsocket.Stop;
ShowNotification(string(res['message'])); AuthService.ClearCredentialId;
except AuthService.Logout;
on E: Exception do window.location.reload(False);
ShowNotification('Delete failed: ' + E.Message); Exit;
end; 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 status = 'ok' then
begin
ShowNotification('Device "' + AName + '" has been deleted.', False); ShowNotification('Device "' + AName + '" has been deleted.', False);
LoadDevices; LoadDevices;
end end
......
...@@ -98,17 +98,22 @@ end; ...@@ -98,17 +98,22 @@ end;
procedure TFViewMap.UpdateDeviceLocation(lat, lng: Double); procedure TFViewMap.UpdateDeviceLocation(lat, lng: Double);
begin begin
if userLocationMarker = nil then lfMap.BeginUpdate;
begin try
userLocationMarker := lfMap.Markers.Add; if userLocationMarker = nil then
userLocationMarker.Title := '<div class="small fw-semibold">You are here</div>'; begin
userLocationMarker.DataString := 'device'; userLocationMarker := lfMap.Markers.Add;
userLocationMarker.IconURL := 'assets/markers/location_dot.png'; userLocationMarker.Title := '<div class="small fw-semibold">You are here</div>';
userLocationMarker.Visible := False; userLocationMarker.DataString := 'device';
end; userLocationMarker.IconURL := 'assets/markers/location_dot.png';
userLocationMarker.Visible := False;
end;
userLocationMarker.Latitude := lat; userLocationMarker.Latitude := lat;
userLocationMarker.Longitude := lng; userLocationMarker.Longitude := lng;
finally
lfMap.EndUpdate;
end;
end; end;
......
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