Commit 28a1f426 by Mac Stephens

Share device management forms, rename Postgres settings, and fix WC data module and map icon errors

parent 80d51ec3
......@@ -8,6 +8,9 @@ Client relocation and separate static outputs are complete; the user confirmed b
## Latest implementation checkpoint
- User committed/pushed preceding cleanup. Moved identical registration and device-manager PAS/DFM/HTML into Shared/Web, with direct references from both projects. No wrappers/subfolders or behavior changes. Both TMS builds passed; generated shared templates, JS credential identities and project XML checked. Runtime screen checks pending. CLI temporary output does not contain all GeoJSON assets; existing deployment assets remain untouched.
- Comparison found remaining WC-to-BFD improvements: map/list HTTP-versus-WebSocket refresh buffering, newer error-page layout, and iOS viewport-height handling. Not changed during form relocation. Preserve BFD fire-specific behavior when addressing these. Auth storage keys intentionally differ; web feature parity is not yet complete.
- User-requested cleanup complete: Common.Ini loads WC [Database] or BFD [Oracle] into the same connection properties and applies them through ConfigureConnection. Startup validates client and Oracle settings once; removed Oracle.Connection and throwaway connection validation. All active CAD/auth/device connections use the common method; no designer fallback.
- Standalone BFD server retired as a supported build/fallback; source and nested history retained with RETIRED.md. Its historical project is no longer expected to build against current shared modules. Only emiMobileServer/emiMobileServer.dproj is supported for both clients.
- Removed shared Ws.Service/Ws.ServiceImpl stubs, unused WebSocketServer, unused LoginRequest and project references, ClearVerificationCode contract/implementations/designer queries, both uqUserPref components, commented old login code and unused WC import in Main. DTOGeneratorUtils retained in tools outside runtime project. No web changes or database modifications.
......@@ -120,3 +123,7 @@ Keep changes minimal and consistent with existing Delphi/TMS conventions. Explai
## Registration SMS failure fix
Changed shared BeginRegistration to save verification codes only after Twilio accepts the HTTP send request. HTTP rejection logs details and returns a friendly error without raising an intentional debugger exception. Transport/configuration exceptions retain the existing error response. Both WC and BFD use this code; no schema changes. Shared server build passed; live failure/success tests pending. Twilio acceptance is not proof of delivery; no delivery callback added. Existing codes are not migrated or cleared by this edit and retain their five-minute expiry.
## INI section naming
Renamed the active shared server INI [Database] section to [Postgres], preserving all values. Common.Ini and startup log labels updated. Client=WC reads [Postgres]; Client=BFD uses [Oracle]. Shared build and offline configuration tests passed. Other deployments must rename their old section when adopting this build; the retired standalone configuration was not changed.
......@@ -47,3 +47,5 @@ BFD browser registration, verification, and login were tested successfully on th
Simplified the client server units by moving response-building code directly into WC.Api.Database and BFD.Api.Database. Removed both Api.Data wrapper units. API and WebSockets still reuse the same methods, with separate database instances. Both server builds passed.
Cleaned up unused server units, queries, and old commented login code. Both database types now use Common.Ini for connection setup. The standalone BFD server is retired; its files remain as reference. The shared server builds, and offline WC/Oracle configuration checks passed.
Registration and device management now use shared forms under Shared/Web. Both web builds and shared-form resource checks passed. WC-to-BFD refresh buffering and presentation improvements remain for a separate step.
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{AF5F5F83-4934-4D6E-9EBC-D50F1604211A}</ProjectGuid>
</PropertyGroup>
<ItemGroup>
<Projects Include="bfdEmiMobile.dproj">
<Dependencies/>
</Projects>
<Projects Include="..\..\WC\Web\wcEmiMobile.dproj">
<Dependencies/>
</Projects>
</ItemGroup>
<ProjectExtensions>
<Borland.Personality>Default.Personality.12</Borland.Personality>
<Borland.ProjectType/>
<BorlandProject>
<Default.Personality/>
</BorlandProject>
</ProjectExtensions>
<Target Name="bfdEmiMobile">
<MSBuild Projects="bfdEmiMobile.dproj"/>
</Target>
<Target Name="bfdEmiMobile:Clean">
<MSBuild Projects="bfdEmiMobile.dproj" Targets="Clean"/>
</Target>
<Target Name="bfdEmiMobile:Make">
<MSBuild Projects="bfdEmiMobile.dproj" Targets="Make"/>
</Target>
<Target Name="wcEmiMobile">
<MSBuild Projects="..\..\WC\Web\wcEmiMobile.dproj"/>
</Target>
<Target Name="wcEmiMobile:Clean">
<MSBuild Projects="..\..\WC\Web\wcEmiMobile.dproj" Targets="Clean"/>
</Target>
<Target Name="wcEmiMobile:Make">
<MSBuild Projects="..\..\WC\Web\wcEmiMobile.dproj" Targets="Make"/>
</Target>
<Target Name="Build">
<CallTarget Targets="bfdEmiMobile;wcEmiMobile"/>
</Target>
<Target Name="Clean">
<CallTarget Targets="bfdEmiMobile:Clean;wcEmiMobile:Clean"/>
</Target>
<Target Name="Make">
<CallTarget Targets="bfdEmiMobile:Make;wcEmiMobile:Make"/>
</Target>
<Import Project="$(BDS)\Bin\CodeGear.Group.Targets" Condition="Exists('$(BDS)\Bin\CodeGear.Group.Targets')"/>
</Project>
......@@ -97,6 +97,8 @@ end;
procedure TFViewMap.UpdateDeviceLocation(lat, lng: Double);
begin
lfMap.BeginUpdate;
try
if userLocationMarker = nil then
begin
userLocationMarker := lfMap.Markers.Add;
......@@ -108,6 +110,9 @@ begin
userLocationMarker.Latitude := lat;
userLocationMarker.Longitude := lng;
finally
lfMap.EndUpdate;
end;
end;
......
......@@ -28,8 +28,8 @@ uses
View.ComplaintArchive in 'View.ComplaintArchive.pas' {FViewComplaintArchive: TWebForm} {*.html},
uMapMarkerJs in 'uMapMarkerJs.pas',
Module.Websocket in 'Module.Websocket.pas' {dmWebsocket: TDataModule},
View.DeviceRegistration in 'View.DeviceRegistration.pas' {FViewDeviceRegistration: TWebForm} {*.html},
View.DeviceManager in 'View.DeviceManager.pas' {FViewDeviceManager: TWebForm} {*.html};
View.DeviceRegistration in '..\..\..\Shared\Web\View.DeviceRegistration.pas' {FViewDeviceRegistration: TWebForm} {*.html},
View.DeviceManager in '..\..\..\Shared\Web\View.DeviceManager.pas' {FViewDeviceManager: TWebForm} {*.html};
{$R *.res}
......
......@@ -200,11 +200,11 @@
<FormType>dfm</FormType>
<DesignClass>TDataModule</DesignClass>
</DCCReference>
<DCCReference Include="View.DeviceRegistration.pas">
<DCCReference Include="..\..\..\Shared\Web\View.DeviceRegistration.pas">
<Form>FViewDeviceRegistration</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="View.DeviceManager.pas">
<DCCReference Include="..\..\..\Shared\Web\View.DeviceManager.pas">
<Form>FViewDeviceManager</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
......
object FViewDeviceManager: TFViewDeviceManager
Width = 900
Height = 600
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
ParentFont = False
OnCreate = WebFormCreate
object pnlMessage: TWebPanel
Left = 8
Top = 8
Width = 200
Height = 33
ElementID = 'view.devmgr.message'
TabOrder = 0
object lblMessage: TWebLabel
Left = 8
Top = 8
Width = 42
Height = 13
Caption = 'Message'
ElementID = 'view.devmgr.message.label'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnCloseNotification: TWebButton
Left = 170
Top = 4
Width = 22
Height = 25
ElementID = 'view.devmgr.message.button'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnCloseNotificationClick
end
end
object edtNewDeviceName: TWebEdit
Left = 8
Top = 50
Width = 175
Height = 25
ElementID = 'view.devmgr.newname'
HeightPercent = 100.000000000000000000
TabOrder = 1
WidthPercent = 100.000000000000000000
end
object edtNewPhoneNumber: TWebEdit
Left = 192
Top = 50
Width = 155
Height = 25
ElementID = 'view.devmgr.newphone'
HeightPercent = 100.000000000000000000
TabOrder = 2
TextHint = '(303) 555-1234'
WidthPercent = 100.000000000000000000
end
object btnAddDevice: TWebButton
Left = 356
Top = 50
Width = 60
Height = 25
Caption = 'Add'
ElementID = 'view.devmgr.btnadd'
HeightPercent = 100.000000000000000000
TabOrder = 3
WidthPercent = 100.000000000000000000
OnClick = btnAddDeviceClick
end
object btnLogout: TWebButton
Left = 720
Top = 50
Width = 75
Height = 25
Caption = 'Log Out'
ElementID = 'view.devmgr.btnlogout'
HeightPercent = 100.000000000000000000
Visible = False
WidthPercent = 100.000000000000000000
OnClick = btnLogoutClick
end
object XDataWebClient: TXDataWebClient
Connection = DMConnection.ApiConnection
Left = 800
Top = 8
end
end
<div class="container-fluid p-3 h-100 d-flex flex-column">
<div class="d-flex align-items-center 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'))">
Refresh
</button>
</div>
<!-- Notification bar -->
<div id="view.devmgr.message"
class="alert alert-danger d-flex align-items-start d-none mb-3"
role="alert">
<span id="view.devmgr.message.label" class="me-auto"></span>
<button id="view.devmgr.message.button"
type="button"
class="btn-close ms-2"
aria-label="Close"></button>
</div>
<!-- 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;">
<input type="text"
id="view.devmgr.newname"
class="form-control form-control-sm"
placeholder="Device name"
required>
<div class="invalid-feedback small">Device name is required.</div>
</div>
<div style="width:160px;">
<input type="tel"
id="view.devmgr.newphone"
class="form-control form-control-sm"
placeholder="(303) 555-1234"
required>
<div class="invalid-feedback small">Phone number is required.</div>
</div>
<div style="width:140px;">
<select id="view.devmgr.newtype"
class="form-select form-select-sm"
required>
<option value="" selected>Device type</option>
<option value="iphone">iPhone</option>
<option value="android">Android</option>
<option value="browser">Browser</option>
<option value="other">Other</option>
</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>
</div>
</div>
<!-- Device table -->
<div class="table-responsive flex-grow-1">
<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:120px;">Phone</th>
<th style="min-width:145px;">Verification Code</th>
<th style="min-width:140px;">Username</th>
<th>Browser / User Agent</th>
<th style="min-width:135px;">Registered</th>
<th style="min-width:80px;">Status</th>
<th style="min-width:85px;">Access</th>
<th style="min-width:75px;">Delete</th>
<th style="min-width:90px;">Send Link</th>
</tr>
</thead>
<tbody id="view.devmgr.tbody">
</tbody>
</table>
<p id="view.devmgr.empty" class="text-muted d-none text-center py-4">
No devices found.
</p>
</div>
<!-- Information modal used by Utils.ShowInformationModal -->
<div class="modal fade" id="main_information_modal" tabindex="-1"
aria-labelledby="main_information_modal_title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-sm mx-auto px-3">
<div class="modal-content shadow-lg">
<div class="modal-header">
<h5 class="modal-title" id="main_information_modal_title">Information</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fs-6 fw-bold" id="main_information_modal_body"></div>
<div class="modal-footer">
<button type="button" class="btn btn-primary w-100" data-bs-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>
<!-- Section: Confirmation modal used by Utils.ShowConfirmationModal -->
<div class="modal fade" id="main_confirmation_modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content shadow-lg">
<div class="modal-header">
<h5 class="modal-title">Confirm</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fw-bold" id="main_modal_body"></div>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-secondary me-3" id="btn_confirm_left">Cancel</button>
<button type="button" class="btn btn-danger" id="btn_confirm_right">Delete</button>
</div>
</div>
</div>
</div>
</div>
object FViewDeviceRegistration: TFViewDeviceRegistration
Width = 640
Height = 480
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
ParentFont = False
OnCreate = WebFormCreate
object edtPhoneNumber: TWebEdit
Left = 240
Top = 136
Width = 121
Height = 21
ElementID = 'view.devicereg.edtphonenumber'
HeightPercent = 100.000000000000000000
TextHint = '(303) 555-1234'
WidthPercent = 100.000000000000000000
end
object btnRegister: TWebButton
Left = 240
Top = 163
Width = 121
Height = 25
Caption = 'Send Code'
ElementID = 'view.devicereg.btnregister'
HeightPercent = 100.000000000000000000
TabOrder = 1
WidthPercent = 100.000000000000000000
OnClick = btnRegisterClick
end
object pnlVerification: TWebPanel
Left = 240
Top = 194
Width = 177
Height = 58
ElementID = 'view.devicereg.verification'
TabOrder = 2
object edtVerificationCode: TWebEdit
Left = 0
Top = 0
Width = 121
Height = 21
ElementID = 'view.devicereg.edtverificationcode'
HeightPercent = 100.000000000000000000
TextHint = 'Six-digit code'
WidthPercent = 100.000000000000000000
end
object btnVerifyCode: TWebButton
Left = 0
Top = 27
Width = 121
Height = 25
Caption = 'Verify and Register'
ElementID = 'view.devicereg.btnverify'
HeightPercent = 100.000000000000000000
TabOrder = 1
WidthPercent = 100.000000000000000000
OnClick = btnVerifyCodeClick
end
end
object pnlMessage: TWebPanel
Left = 240
Top = 65
Width = 121
Height = 33
ElementID = 'view.devicereg.message'
TabOrder = 3
object lblMessage: TWebLabel
Left = 16
Top = 11
Width = 42
Height = 13
Caption = 'Message'
ElementID = 'view.devicereg.message.label'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnCloseNotification: TWebButton
Left = 96
Top = 3
Width = 22
Height = 25
ElementID = 'view.devicereg.message.button'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnCloseNotificationClick
end
end
object XDataWebClient: TXDataWebClient
Connection = DMConnection.AuthConnection
Left = 492
Top = 102
end
end
<div class="container">
<nav class="navbar navbar-light bg-light border rounded-bottom"
style="--bs-border-color: #d6d6d6; --bs-border-radius: 0.25rem;">
<div class="container-fluid">
<a id="view.devicereg.apptitle"
class="navbar-brand text-secondary small"
href="index.html">
emiMobile
</a>
</div>
</nav>
<div class="row justify-content-center mt-4">
<div class="col-12 col-sm-10 col-md-8 col-lg-5 col-xl-4">
<div class="card shadow-sm">
<div class="card-header">
<h5 class="card-title mb-0 text-center">
Register This Device
</h5>
</div>
<div class="card-body">
<!-- Notification bar -->
<div id="view.devicereg.message"
class="alert alert-danger d-flex align-items-start d-none mb-3"
role="alert">
<span id="view.devicereg.message.label" class="me-auto"></span>
<button id="view.devicereg.message.button"
type="button"
class="btn-close ms-2"
aria-label="Close"></button>
</div>
<p class="text-muted small mb-3">
This browser has not been registered for emiMobile access.
Enter the phone number your administrator registered for this device,
then click <strong>Send Code</strong>.
</p>
<div class="mb-3">
<label class="form-label small text-muted">Phone number</label>
<input id="view.devicereg.edtphonenumber"
class="form-control"
type="tel"
placeholder="(303) 555-1234"
autofocus>
</div>
<div id="view.devicereg.verification" class="mb-3 d-none">
<label class="form-label small text-muted">Verification code</label>
<input id="view.devicereg.edtverificationcode"
class="form-control mb-2"
type="text"
inputmode="numeric"
autocomplete="one-time-code"
maxlength="6"
placeholder="Six-digit code">
<button id="view.devicereg.btnverify"
class="btn btn-primary w-100">
Verify and Register
</button>
</div>
<div class="mb-3">
<p class="text-muted small mb-1">Browser</p>
<p id="view.devicereg.useragent"
class="text-muted small text-truncate"
style="max-width:100%; overflow:hidden; white-space:nowrap;"></p>
</div>
<button id="view.devicereg.btnregister"
class="btn btn-outline-primary w-100">
Send Code
</button>
</div>
<div class="card-footer text-muted small">
Registration is required once per browser. Clearing browser storage will require re-registration.
</div>
</div>
</div>
</div>
</div>
unit View.DeviceRegistration;
interface
uses
System.SysUtils, System.Classes, WEBLib.Graphics, WEBLib.Controls, WEBLib.Forms, WEBLib.Dialogs,
Vcl.Controls, Vcl.StdCtrls, WEBLib.StdCtrls, WEBLib.JSON,
JS, XData.Web.Connection, WEBLib.ExtCtrls,
App.Types, ConnectionModule, XData.Web.Client;
type
TFViewDeviceRegistration = class(TWebForm)
edtPhoneNumber: TWebEdit;
btnRegister: TWebButton;
pnlVerification: TWebPanel;
edtVerificationCode: TWebEdit;
btnVerifyCode: TWebButton;
pnlMessage: TWebPanel;
lblMessage: TWebLabel;
btnCloseNotification: TWebButton;
XDataWebClient: TXDataWebClient;
procedure btnRegisterClick(Sender: TObject);
procedure btnVerifyCodeClick(Sender: TObject);
procedure btnCloseNotificationClick(Sender: TObject);
procedure WebFormCreate(Sender: TObject);
private
FRegistrationProc: TSuccessProc;
procedure ShowNotification(const AMsg: string; AIsError: Boolean = True);
procedure HideNotification;
procedure SetBusy(ABusy: Boolean);
procedure SetVerifyBusy(ABusy: Boolean);
procedure DoWebAuthnCreate(APhoneNumber, AChallenge, AChallengeToken: string);
procedure DoSimpleKeyCreate(APhoneNumber, AChallengeToken: string);
public
class procedure Display(ARegistrationProc: TSuccessProc);
end;
var
FViewDeviceRegistration: TFViewDeviceRegistration;
implementation
uses
Auth.Service,
View.ErrorPage;
{$R *.dfm}
class procedure TFViewDeviceRegistration.Display(ARegistrationProc: TSuccessProc);
begin
if Assigned(FViewDeviceRegistration) then
FViewDeviceRegistration.Free;
FViewDeviceRegistration := TFViewDeviceRegistration.CreateNew;
FViewDeviceRegistration.FRegistrationProc := ARegistrationProc;
end;
procedure TFViewDeviceRegistration.WebFormCreate(Sender: TObject);
var
userAgent: string;
begin
HideNotification;
pnlVerification.ElementHandle.classList.add('d-none');
userAgent := '';
asm
userAgent = navigator.userAgent;
end;
asm
var el = document.getElementById('view.devicereg.useragent');
if (el) el.textContent = userAgent;
// Format phone number on-the-fly as the user types
var inp = document.getElementById('view.devicereg.edtphonenumber');
if (inp) {
inp.addEventListener('input', function() {
var digits = inp.value.replace(/\D/g, '');
if (digits.length > 10) digits = digits.slice(0, 10);
var formatted = '';
if (digits.length > 6)
formatted = '(' + digits.slice(0,3) + ') ' + digits.slice(3,6) + '-' + digits.slice(6);
else if (digits.length > 3)
formatted = '(' + digits.slice(0,3) + ') ' + digits.slice(3);
else if (digits.length > 0)
formatted = '(' + digits;
inp.value = formatted;
});
}
end;
end;
procedure TFViewDeviceRegistration.SetBusy(ABusy: Boolean);
begin
asm
var btn = document.getElementById('view.devicereg.btnregister');
if (btn) {
btn.disabled = ABusy;
btn.textContent = ABusy ? 'Sending...' : 'Send Code';
}
end;
end;
procedure TFViewDeviceRegistration.SetVerifyBusy(ABusy: Boolean);
begin
asm
var btn = document.getElementById('view.devicereg.btnverify');
if (btn) {
btn.disabled = ABusy;
btn.textContent = ABusy ? 'Verifying...' : 'Verify and Register';
}
end;
end;
procedure TFViewDeviceRegistration.btnRegisterClick(Sender: TObject);
var
phoneNumber: string;
procedure OnCodeSent;
begin
SetBusy(False);
edtPhoneNumber.Enabled := False;
pnlVerification.ElementHandle.classList.remove('d-none');
edtVerificationCode.SetFocus;
ShowNotification('Verification code sent. Enter the six-digit code to continue.', False);
end;
procedure OnCodeError(AMsg: string);
begin
SetBusy(False);
ShowNotification(AMsg);
end;
begin
phoneNumber := Trim(edtPhoneNumber.Text);
if phoneNumber = '' then
begin
ShowNotification('Please enter your phone number.');
Exit;
end;
SetBusy(True);
HideNotification;
AuthService.BeginRegistration(phoneNumber, @OnCodeSent, @OnCodeError);
end;
procedure TFViewDeviceRegistration.btnVerifyCodeClick(Sender: TObject);
var
phoneNumber, verificationCode: string;
i: Integer;
procedure OnVerified(AChallenge, AChallengeToken: string);
begin
DoSimpleKeyCreate(phoneNumber, AChallengeToken);
end;
procedure OnVerifyError(AMsg: string);
begin
SetVerifyBusy(False);
ShowNotification(AMsg);
end;
begin
phoneNumber := Trim(edtPhoneNumber.Text);
verificationCode := Trim(edtVerificationCode.Text);
if Length(verificationCode) <> 6 then
begin
ShowNotification('Please enter the six-digit verification code.');
Exit;
end;
for i := 1 to Length(verificationCode) do
if (verificationCode[i] < '0') or (verificationCode[i] > '9') then
begin
ShowNotification('Please enter the six-digit verification code.');
Exit;
end;
SetVerifyBusy(True);
HideNotification;
AuthService.VerifyRegistrationCode(phoneNumber, verificationCode,
@OnVerified, @OnVerifyError);
end;
procedure TFViewDeviceRegistration.DoWebAuthnCreate(
APhoneNumber, AChallenge, AChallengeToken: string);
var
phoneNumber, challenge, challengeToken: string;
procedure OnCompleteOK;
begin
FRegistrationProc;
end;
procedure OnCompleteError(AMsg: string);
begin
SetVerifyBusy(False);
ShowNotification(AMsg);
end;
procedure OnCredential(ACredentialId, AAttestationObject, AClientDataJSON: string);
begin
AuthService.CompleteRegistration(
phoneNumber, ACredentialId, AAttestationObject, AClientDataJSON, challengeToken,
@OnCompleteOK, @OnCompleteError
);
end;
procedure OnWebAuthnError(AMsg: string);
begin
SetVerifyBusy(False);
ShowNotification(AMsg);
end;
begin
phoneNumber := APhoneNumber;
challenge := AChallenge;
challengeToken := AChallengeToken;
asm
(function() {
function b64urlToArr(b64) {
b64 = b64.replace(/-/g, '+').replace(/_/g, '/');
while (b64.length % 4) b64 += '=';
var bin = atob(b64);
var arr = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
return arr;
}
function arrToB64url(buf) {
var bin = String.fromCharCode.apply(null, new Uint8Array(buf));
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
var userId = new Uint8Array(16);
crypto.getRandomValues(userId);
navigator.credentials.create({
publicKey: {
challenge: b64urlToArr(challenge),
rp: { id: window.location.hostname, name: 'emiMobile' },
user: { id: userId, name: 'emimobile-user', displayName: 'emiMobile User' },
pubKeyCredParams: [{ alg: -7, type: 'public-key' }],
timeout: 60000,
attestation: 'none',
authenticatorSelection: {
userVerification: 'required',
residentKey: 'preferred'
}
}
}).then(function(cred) {
var credId = arrToB64url(cred.rawId);
var attObj = arrToB64url(cred.response.attestationObject);
var cdJson = arrToB64url(cred.response.clientDataJSON);
OnCredential(credId, attObj, cdJson);
}).catch(function(err) {
OnWebAuthnError('WebAuthn error: ' + err.message);
});
})();
end;
end;
procedure TFViewDeviceRegistration.DoSimpleKeyCreate(APhoneNumber, AChallengeToken: string);
var
phoneNumber, challengeToken, deviceKey: string;
procedure OnCompleteOK;
begin
FRegistrationProc;
end;
procedure OnCompleteError(AMsg: string);
begin
SetVerifyBusy(False);
ShowNotification(AMsg);
end;
begin
phoneNumber := APhoneNumber;
challengeToken := AChallengeToken;
deviceKey := '';
asm
var keyBytes = new Uint8Array(32);
crypto.getRandomValues(keyBytes);
var bin = String.fromCharCode.apply(null, keyBytes);
deviceKey = btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
end;
if deviceKey = '' then
begin
SetVerifyBusy(False);
ShowNotification('Failed to generate device key.');
Exit;
end;
AuthService.CompleteRegistrationSimple(
phoneNumber, deviceKey, challengeToken,
@OnCompleteOK, @OnCompleteError
);
end;
procedure TFViewDeviceRegistration.btnCloseNotificationClick(Sender: TObject);
begin
HideNotification;
end;
procedure TFViewDeviceRegistration.ShowNotification(const AMsg: string; AIsError: Boolean);
begin
if AMsg <> '' then
begin
lblMessage.Caption := AMsg;
asm
var el = document.getElementById('view.devicereg.message');
if (el) {
el.classList.remove('alert-danger', 'alert-warning');
el.classList.add(AIsError ? 'alert-danger' : 'alert-warning');
el.classList.remove('d-none');
}
end;
end;
end;
procedure TFViewDeviceRegistration.HideNotification;
begin
asm
var el = document.getElementById('view.devicereg.message');
if (el) el.classList.add('d-none');
end;
end;
end.
......@@ -98,6 +98,8 @@ end;
procedure TFViewMap.UpdateDeviceLocation(lat, lng: Double);
begin
lfMap.BeginUpdate;
try
if userLocationMarker = nil then
begin
userLocationMarker := lfMap.Markers.Add;
......@@ -109,6 +111,9 @@ begin
userLocationMarker.Latitude := lat;
userLocationMarker.Longitude := lng;
finally
lfMap.EndUpdate;
end;
end;
......
......@@ -28,8 +28,8 @@ uses
View.ComplaintArchive in 'View.ComplaintArchive.pas' {FViewComplaintArchive: TWebForm} {*.html},
uMapMarkerJs in 'uMapMarkerJs.pas',
Module.Websocket in 'Module.Websocket.pas' {dmWebsocket: TDataModule},
View.DeviceRegistration in 'View.DeviceRegistration.pas' {FViewDeviceRegistration: TWebForm} {*.html},
View.DeviceManager in 'View.DeviceManager.pas' {FViewDeviceManager: TWebForm} {*.html};
View.DeviceRegistration in '..\..\..\Shared\Web\View.DeviceRegistration.pas' {FViewDeviceRegistration: TWebForm} {*.html},
View.DeviceManager in '..\..\..\Shared\Web\View.DeviceManager.pas' {FViewDeviceManager: TWebForm} {*.html};
{$R *.res}
......
......@@ -201,11 +201,11 @@
<FormType>dfm</FormType>
<DesignClass>TDataModule</DesignClass>
</DCCReference>
<DCCReference Include="View.DeviceRegistration.pas">
<DCCReference Include="..\..\..\Shared\Web\View.DeviceRegistration.pas">
<Form>FViewDeviceRegistration</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
<DCCReference Include="View.DeviceManager.pas">
<DCCReference Include="..\..\..\Shared\Web\View.DeviceManager.pas">
<Form>FViewDeviceManager</Form>
<DesignClass>TWebForm</DesignClass>
</DCCReference>
......
object PostgreSQLDeviceDatabase: TPostgreSQLDeviceDatabase
Left = 0
Top = 0
ClientHeight = 703
ClientWidth = 774
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -12
Font.Name = 'Segoe UI'
Font.Style = []
TextHeight = 15
Height = 703
Width = 774
object ucDevices: TUniConnection
ProviderName = 'PostgreSQL'
LoginPrompt = False
......
......@@ -27,7 +27,7 @@ type
FTwilioFromNumber: string;
FTwilioFromNumberFromIni: Boolean;
// [Database]
// Section: Database settings
FDatabaseServer: string;
FDatabaseServerFromIni: Boolean;
FDatabasePort: Integer;
......@@ -64,7 +64,7 @@ type
property TwilioFromNumber: string read FTwilioFromNumber;
property TwilioFromNumberFromIni: Boolean read FTwilioFromNumberFromIni;
// [Database]
// Section: Database settings
property DatabaseServer: string read FDatabaseServer;
property DatabaseServerFromIni: Boolean read FDatabaseServerFromIni;
property DatabasePort: Integer read FDatabasePort;
......@@ -125,21 +125,21 @@ begin
FTwilioFromNumber := iniFile.ReadString('Twilio', 'FromNumber', '');
FTwilioFromNumberFromIni := iniFile.ValueExists('Twilio', 'FromNumber');
// [Database]
FDatabaseServer := iniFile.ReadString('Database', 'Server', '');
FDatabaseServerFromIni := iniFile.ValueExists('Database', 'Server');
// Section: Postgres connection settings
FDatabaseServer := iniFile.ReadString('Postgres', 'Server', '');
FDatabaseServerFromIni := iniFile.ValueExists('Postgres', 'Server');
FDatabasePort := iniFile.ReadInteger('Database', 'Port', 5432);
FDatabasePortFromIni := iniFile.ValueExists('Database', 'Port');
FDatabasePort := iniFile.ReadInteger('Postgres', 'Port', 5432);
FDatabasePortFromIni := iniFile.ValueExists('Postgres', 'Port');
FDatabaseName := iniFile.ReadString('Database', 'Database', 'lems_wcso');
FDatabaseNameFromIni := iniFile.ValueExists('Database', 'Database');
FDatabaseName := iniFile.ReadString('Postgres', 'Database', 'lems_wcso');
FDatabaseNameFromIni := iniFile.ValueExists('Postgres', 'Database');
FDatabaseUsername := iniFile.ReadString('Database', 'Username', 'postgres');
FDatabaseUsernameFromIni := iniFile.ValueExists('Database', 'Username');
FDatabaseUsername := iniFile.ReadString('Postgres', 'Username', 'postgres');
FDatabaseUsernameFromIni := iniFile.ValueExists('Postgres', 'Username');
FDatabasePassword := iniFile.ReadString('Database', 'Password', '');
FDatabasePasswordFromIni := iniFile.ValueExists('Database', 'Password');
FDatabasePassword := iniFile.ReadString('Postgres', 'Password', '');
FDatabasePasswordFromIni := iniFile.ValueExists('Postgres', 'Password');
// Note: Oracle uses a TNS alias; PostgreSQL uses host, port, and database.
......
......@@ -194,12 +194,12 @@ begin
Logger.Log(1, '--- Oracle database: using [Oracle] INI settings ---')
else
begin
Logger.Log(1, '--- Database ---');
Logger.Log(1, LogValue('--Database->Server', IniEntries.DatabaseServer, IniEntries.DatabaseServerFromIni));
Logger.Log(1, LogValue('--Database->Port', IniEntries.DatabasePort.ToString, IniEntries.DatabasePortFromIni));
Logger.Log(1, LogValue('--Database->Name', IniEntries.DatabaseName, IniEntries.DatabaseNameFromIni));
Logger.Log(1, LogValue('--Database->Username', IniEntries.DataBaseUsername, IniEntries.DatabaseUsernameFromIni));
Logger.Log(1, LogValue('--Database->Password', '[configured]', IniEntries.DatabasePasswordFromIni));
Logger.Log(1, '--- Postgres ---');
Logger.Log(1, LogValue('--Postgres->Server', IniEntries.DatabaseServer, IniEntries.DatabaseServerFromIni));
Logger.Log(1, LogValue('--Postgres->Port', IniEntries.DatabasePort.ToString, IniEntries.DatabasePortFromIni));
Logger.Log(1, LogValue('--Postgres->Name', IniEntries.DatabaseName, IniEntries.DatabaseNameFromIni));
Logger.Log(1, LogValue('--Postgres->Username', IniEntries.DataBaseUsername, IniEntries.DatabaseUsernameFromIni));
Logger.Log(1, LogValue('--Postgres->Password', '[configured]', IniEntries.DatabasePasswordFromIni));
end;
Logger.Log(1, '');
......
[Application]
Client=BFD
Client=WC
[Settings]
LogFileNum=172
webClientVersion=9.4.0
LogFileNum=180
webClientVersion=0.9.4.9
MemoLogLevel=3
FileLogLevel=4
......@@ -12,7 +12,7 @@ AccountSid=AC717f06b3c10bbecff42487426e34fdb3
AuthToken=a0dec128c1b5a3231909ba81da8947b6
FromNumber=+18775346578
[Database]
[Postgres]
--Server=192.168.91.129
Server=192.168.102.10
--Server=192.168.56.129
......
......@@ -10,6 +10,7 @@ emiMobileServer/
Oracle/ Oracle device storage and connection settings
PostgreSQL/ PostgreSQL device storage
Models/ Request, response, and WebSocket message types
Web/ Shared registration and device-management forms
Clients/
WC/
Server/ Wyoming CAD and authentication modules
......@@ -36,3 +37,5 @@ The main project's former Source, Core, and Database directories have been conso
Each client Server folder now contains Api.Database (designer queries plus response-building methods) and Auth.Database (login/user/agency queries). The separate Api.Data wrapper units have been removed. Shared/Clients/Client.Data remains the common data-module contract used by client selection, API services, and WebSockets.
Connection settings for both clients are loaded and applied by Shared/Server/Common.Ini.pas. The unused DTO generator is retained in tools/ outside the application project.
The shared server INI uses [Postgres] for WC and [Oracle] for BFD. [Database] is no longer read by the shared server; the Database key inside [Postgres] still names the PostgreSQL database.
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