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>
......
...@@ -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