Commit 97fe9d07 by Mac Stephens

Clean up retired code, finish BFD parity fixes, and support registration by phone and device type

parent 445d8738
......@@ -383,6 +383,9 @@ begin
DataSet.FieldByName('complaintStatusKey').AsString := statusKey;
// Note: Held-for-unit incidents use the existing hold marker images.
if statusKey = 'holdu' then statusKey := 'hold';
if Trim(rawCategory) = '' then
DataSet.FieldByName('pngName').AsString := Format('default_%s.png', [statusKey])
else
......@@ -465,8 +468,15 @@ begin
end;
procedure TBFDAPIDataModule.ConfigureQueryTimeouts;
const
COMMAND_TIMEOUT_SECONDS = 15;
begin
// Note: Preserve the existing Oracle query timeout settings.
uqBadgeCounts.SpecificOptions.Values['CommandTimeout'] := IntToStr(COMMAND_TIMEOUT_SECONDS);
uqMapUnits.SpecificOptions.Values['CommandTimeout'] := IntToStr(COMMAND_TIMEOUT_SECONDS);
uqMapComplaintUnitsList.SpecificOptions.Values['CommandTimeout'] := IntToStr(COMMAND_TIMEOUT_SECONDS);
uqMapComplaints.SpecificOptions.Values['CommandTimeout'] := IntToStr(COMMAND_TIMEOUT_SECONDS);
uqUnitList.SpecificOptions.Values['CommandTimeout'] := IntToStr(COMMAND_TIMEOUT_SECONDS);
uqComplaintList.SpecificOptions.Values['CommandTimeout'] := IntToStr(COMMAND_TIMEOUT_SECONDS);
end;
function TBFDAPIDataModule.GetBadgeCounts: TJSONObject;
......
......@@ -4,6 +4,13 @@ object BFDAuthDatabase: TBFDAuthDatabase
Height = 373
Width = 649
PixelsPerInch = 144
object uqNextAuditId: TUniQuery
Connection = ucENTCAD
SQL.Strings = (
'SELECT SEQ_AUDIT_MASTERID.NEXTVAL AS NEXTSEQVAL FROM DUAL')
Left = 450
Top = 75
end
object uqMisc: TUniQuery
Connection = ucENTCAD
FetchRows = 100
......
......@@ -9,6 +9,7 @@ uses
type
TBFDAuthDatabase = class(TClientAuth)
uqMisc: TUniQuery;
uqNextAuditId: TUniQuery;
uqAuth: TUniQuery;
uqAgencies: TUniQuery;
uqAgencyConfig: TUniQuery;
......@@ -139,7 +140,12 @@ begin
if ServerConfig.auditEnabled then
begin
auditMasterId := GetNextSeqVal(uqMisc, 'SEQ_AUDIT_MASTERID');
uqNextAuditId.Open;
try
auditMasterId := uqNextAuditId.FieldByName('NEXTSEQVAL').AsString;
finally
uqNextAuditId.Close;
end;
SetMasterAuditEntry(uqMisc, auditMasterId, 'AUTH', '', agency, personnelid, fullname, 'Login', '', 'emiMobile');
end
else
......
......@@ -51,13 +51,13 @@ type
procedure ClearCredentialId;
// Section: Device registration verification
procedure BeginRegistration(APhoneNumber: string;
procedure BeginRegistration(APhoneNumber, ADeviceType: string;
ASuccess: TOnCodeSentSuccess; AError: TOnDeviceError);
procedure VerifyRegistrationCode(APhoneNumber, AVerificationCode: string;
procedure VerifyRegistrationCode(APhoneNumber, ADeviceType, AVerificationCode: string;
ASuccess: TOnBeginSuccess;
AError: TOnDeviceError);
// Section: WebAuthn registration
procedure CompleteRegistration(APhoneNumber, ACredentialId,
procedure CompleteRegistration(APhoneNumber, ADeviceType, ACredentialId,
AAttestationObject, AClientDataJSON, AChallengeToken: string;
ASuccess: TOnDeviceSuccess; AError: TOnDeviceError);
......@@ -74,7 +74,7 @@ type
ASuccess: TOnLoginSuccess; AError: TOnLoginError);
// Simple-key registration and login
procedure CompleteRegistrationSimple(APhoneNumber, ADeviceKey, AChallengeToken: string;
procedure CompleteRegistrationSimple(APhoneNumber, ADeviceType, ADeviceKey, AChallengeToken: string;
ASuccess: TOnDeviceSuccess; AError: TOnDeviceError);
procedure LoginDeviceKey(ACredentialId, AChallengeToken: string;
ASuccess: TOnLoginSuccess; AError: TOnLoginError);
......@@ -218,7 +218,7 @@ end;
// ---- WebAuthn registration ----
procedure TAuthService.BeginRegistration(APhoneNumber: string;
procedure TAuthService.BeginRegistration(APhoneNumber, ADeviceType: string;
ASuccess: TOnCodeSentSuccess; AError: TOnDeviceError);
procedure OnLoad(Response: TXDataClientResponse);
......@@ -244,12 +244,12 @@ procedure TAuthService.BeginRegistration(APhoneNumber: string;
begin
FClient.RawInvoke(
'IAuthService.BeginRegistration',
[APhoneNumber],
[APhoneNumber, ADeviceType],
@OnLoad, @OnError
);
end;
procedure TAuthService.VerifyRegistrationCode(APhoneNumber,
procedure TAuthService.VerifyRegistrationCode(APhoneNumber, ADeviceType,
AVerificationCode: string; ASuccess: TOnBeginSuccess;
AError: TOnDeviceError);
......@@ -278,12 +278,12 @@ procedure TAuthService.VerifyRegistrationCode(APhoneNumber,
begin
FClient.RawInvoke(
'IAuthService.VerifyRegistrationCode',
[APhoneNumber, AVerificationCode],
[APhoneNumber, ADeviceType, AVerificationCode],
@OnLoad, @OnError
);
end;
procedure TAuthService.CompleteRegistration(APhoneNumber, ACredentialId,
procedure TAuthService.CompleteRegistration(APhoneNumber, ADeviceType, ACredentialId,
AAttestationObject, AClientDataJSON, AChallengeToken: string;
ASuccess: TOnDeviceSuccess; AError: TOnDeviceError);
......@@ -317,7 +317,7 @@ procedure TAuthService.CompleteRegistration(APhoneNumber, ACredentialId,
begin
FClient.RawInvoke(
'IAuthService.CompleteRegistration',
[APhoneNumber, ACredentialId, AAttestationObject, AClientDataJSON, AChallengeToken],
[APhoneNumber, ADeviceType, ACredentialId, AAttestationObject, AClientDataJSON, AChallengeToken],
@OnLoad, @OnError
);
end;
......@@ -431,7 +431,7 @@ end;
// ---- Simple-key registration and login ----
procedure TAuthService.CompleteRegistrationSimple(APhoneNumber, ADeviceKey, AChallengeToken: string;
procedure TAuthService.CompleteRegistrationSimple(APhoneNumber, ADeviceType, ADeviceKey, AChallengeToken: string;
ASuccess: TOnDeviceSuccess; AError: TOnDeviceError);
procedure OnLoad(Response: TXDataClientResponse);
......@@ -462,7 +462,7 @@ procedure TAuthService.CompleteRegistrationSimple(APhoneNumber, ADeviceKey, ACha
begin
FClient.RawInvoke(
'IAuthService.CompleteRegistrationSimple',
[APhoneNumber, ADeviceKey, AChallengeToken],
[APhoneNumber, ADeviceType, ADeviceKey, AChallengeToken],
@OnLoad, @OnError
);
end;
......
<div class="container">
<br />
<div class="row justify-content-center mt-5">
<div class="col-12 col-sm-10 col-md-8 col-lg-6">
<div class="card shadow-sm border-danger">
<div class="panel panel-red">
<div id="view.errorpage.title" class="panel-heading">
Error Page
<div id="view.errorpage.title"
class="card-header bg-danger text-white fw-semibold">
Error
</div>
<div id="view.errorpage.message" class="panel-body">
<div class="card-body">
<div id="view.errorpage.message"
class="text-danger">
Message
</div>
</div>
<div class="panel-footer">
<a href=".">Reload web application</a>
<div class="card-footer bg-light text-end">
<a href="."
class="btn btn-outline-danger btn-sm">
Reload web application
</a>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex flex-column vh-100">
<div class="d-flex flex-column vh-100 emimobile-main">
<!-- Top Nav -->
<nav id="top_nav" class="navbar navbar-light bg-primary border-light text-light py-2 flex-shrink-0">
......
......@@ -40,6 +40,7 @@ type
FPendingFocusCoord: TTMSFNCMapsCoordinateRec;
FPendingFocusZoom: Integer;
FDoFocusZoom: Boolean;
FPendingFocusMarkerData: string;
[async] procedure LoadPointsAsync(showBusy: Boolean);
procedure PlaceUnitMarkers(aData: TJSArray);
......@@ -606,15 +607,47 @@ begin
end;
procedure TFViewMap.tmrLocateTimer(Sender: TObject);
var
i: Integer;
markerFound: Boolean;
missingTarget: string;
begin
tmrLocate.Enabled := False;
if not FDoFocusZoom then
Exit;
if FPendingFocusMarkerData <> '' then
begin
markerFound := False;
for i := 0 to lfMap.Markers.Count - 1 do
if SameText(lfMap.Markers[i].DataString, FPendingFocusMarkerData) or
StartsText(FPendingFocusMarkerData + '|', lfMap.Markers[i].DataString) then
begin
markerFound := True;
Break;
end;
if not markerFound then
begin
missingTarget := FPendingFocusMarkerData;
FPendingFocusMarkerData := '';
FDoFocusZoom := False;
if StartsText('unit|', missingTarget) then
ShowInformationModal('No Longer Available',
'This unit is no longer available on the map.')
else if StartsText('complaint|', missingTarget) then
ShowInformationModal('No Longer Available',
'This incident is no longer active on the map.');
Exit;
end;
end;
lfMap.SetCenterCoordinate(FPendingFocusCoord);
lfMap.SetZoomLevel(FPendingFocusZoom);
FPendingFocusMarkerData := '';
FDoFocusZoom := False;
end;
......@@ -622,6 +655,12 @@ procedure TFViewMap.btnFindLocationClick(Sender: TObject);
var
coord: TTMSFNCMapsCoordinateRec;
begin
tmrLocate.Enabled := False;
FDoFocusZoom := False;
FPendingFocusMarkerData := '';
FPendingUnitId := '';
FPendingComplaintId := '';
if userLocationMarker = nil then
Exit;
......@@ -639,6 +678,10 @@ end;
procedure TFViewMap.FocusUnit(const unitId: string);
begin
tmrLocate.Enabled := False;
FDoFocusZoom := False;
FPendingFocusMarkerData := '';
FPendingComplaintId := '';
FPendingUnitId := Trim(unitId);
if mapFilters <> nil then
LoadPointsAsync(True);
......@@ -647,6 +690,10 @@ end;
procedure TFViewMap.FocusComplaint(const complaintId: string);
begin
tmrLocate.Enabled := False;
FDoFocusZoom := False;
FPendingFocusMarkerData := '';
FPendingUnitId := '';
FPendingComplaintId := Trim(complaintId);
if mapFilters <> nil then
LoadPointsAsync(True);
......@@ -678,6 +725,7 @@ begin
FPendingFocusCoord := coord;
FPendingFocusZoom := 17;
FDoFocusZoom := True;
FPendingFocusMarkerData := targetDs;
tmrLocate.Interval := 250;
tmrLocate.Enabled := True;
......@@ -687,8 +735,15 @@ begin
end;
end;
if found then
FPendingUnitId := '';
FPendingUnitId := '';
if not found then
begin
FPendingFocusMarkerData := '';
FDoFocusZoom := False;
ShowInformationModal('No Longer Available',
'This unit is no longer available on the map.');
end;
end;
procedure TFViewMap.ApplyPendingComplaintFocus;
......@@ -716,6 +771,7 @@ begin
FPendingFocusCoord := coord;
FPendingFocusZoom := 17;
FDoFocusZoom := True;
FPendingFocusMarkerData := targetDs;
tmrLocate.Interval := 250;
tmrLocate.Enabled := True;
......@@ -725,8 +781,15 @@ begin
end;
end;
if found then
FPendingComplaintId := '';
FPendingComplaintId := '';
if not found then
begin
FPendingFocusMarkerData := '';
FDoFocusZoom := False;
ShowInformationModal('No Longer Available',
'This incident is no longer active on the map.');
end;
end;
......
......@@ -282,6 +282,10 @@
<ResourceType>RCDATA</ResourceType>
<ResourceId>PngImage_17</ResourceId>
</RcItem>
<RcItem Include="assets\markers\location_dot.png">
<ResourceType>RCDATA</ResourceType>
<ResourceId>LocationDot</ResourceId>
</RcItem>
<RcItem Include="assets\markers\default_hold.png">
<ResourceType>RCDATA</ResourceType>
<ResourceId>PngImage_18</ResourceId>
......@@ -623,6 +627,12 @@
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="assets\markers\location_dot.png" Configuration="Debug" Class="ProjectFile">
<Platform Name="Win32">
<RemoteDir>.\</RemoteDir>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="assets\markers\default_hold.png" Configuration="Debug" Class="ProjectFile">
<Platform Name="Win32">
<RemoteDir>.\</RemoteDir>
......
......@@ -186,3 +186,12 @@ html, body {
}
}
/* Note: Keep navigation inside the visible mobile viewport and device safe areas. */
@supports (height: 100dvh) {
.emimobile-main { height: 100dvh !important; }
}
@supports (padding-top: env(safe-area-inset-top)) {
#top_nav { padding-top: calc(0.5rem + env(safe-area-inset-top)) !important; }
#bottom_nav { padding-bottom: calc(0.5rem + env(safe-area-inset-bottom)) !important; }
}
......@@ -6,7 +6,7 @@
<Projects Include="bfdEmiMobile.dproj">
<Dependencies/>
</Projects>
<Projects Include="..\..\..\..\emiMobile_BFD\emiMobileServer\emiMobileServer.dproj">
<Projects Include="..\..\..\emiMobileServer.dproj">
<Dependencies/>
</Projects>
</ItemGroup>
......@@ -27,13 +27,13 @@
<MSBuild Projects="bfdEmiMobile.dproj" Targets="Make"/>
</Target>
<Target Name="emiMobileServer">
<MSBuild Projects="..\..\..\..\emiMobile_BFD\emiMobileServer\emiMobileServer.dproj"/>
<MSBuild Projects="..\..\..\emiMobileServer.dproj"/>
</Target>
<Target Name="emiMobileServer:Clean">
<MSBuild Projects="..\..\..\..\emiMobile_BFD\emiMobileServer\emiMobileServer.dproj" Targets="Clean"/>
<MSBuild Projects="..\..\..\emiMobileServer.dproj" Targets="Clean"/>
</Target>
<Target Name="emiMobileServer:Make">
<MSBuild Projects="..\..\..\..\emiMobile_BFD\emiMobileServer\emiMobileServer.dproj" Targets="Make"/>
<MSBuild Projects="..\..\..\emiMobileServer.dproj" Targets="Make"/>
</Target>
<Target Name="Build">
<CallTarget Targets="bfdEmiMobile;emiMobileServer"/>
......
......@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1" name="viewport"/>
<meta content="width=device-width, initial-scale=1, viewport-fit=cover" name="viewport"/>
<noscript>Your browser does not support JavaScript!</noscript>
<link href="data:;base64,=" rel="icon"/>
<title>emiMobile</title>
......
......@@ -297,7 +297,7 @@ begin
}
if (meta.complaintStatus === 'holdu') {
return 'assets/markers/default_holdu.png';
return 'assets/markers/default_hold.png';
}
if (meta.complaintStatus === 'suspend') {
......
......@@ -51,13 +51,13 @@ type
procedure ClearCredentialId;
// Section: Device registration verification
procedure BeginRegistration(APhoneNumber: string;
procedure BeginRegistration(APhoneNumber, ADeviceType: string;
ASuccess: TOnCodeSentSuccess; AError: TOnDeviceError);
procedure VerifyRegistrationCode(APhoneNumber, AVerificationCode: string;
procedure VerifyRegistrationCode(APhoneNumber, ADeviceType, AVerificationCode: string;
ASuccess: TOnBeginSuccess;
AError: TOnDeviceError);
// Section: WebAuthn registration
procedure CompleteRegistration(APhoneNumber, ACredentialId,
procedure CompleteRegistration(APhoneNumber, ADeviceType, ACredentialId,
AAttestationObject, AClientDataJSON, AChallengeToken: string;
ASuccess: TOnDeviceSuccess; AError: TOnDeviceError);
......@@ -74,7 +74,7 @@ type
ASuccess: TOnLoginSuccess; AError: TOnLoginError);
// Simple-key registration and login
procedure CompleteRegistrationSimple(APhoneNumber, ADeviceKey, AChallengeToken: string;
procedure CompleteRegistrationSimple(APhoneNumber, ADeviceType, ADeviceKey, AChallengeToken: string;
ASuccess: TOnDeviceSuccess; AError: TOnDeviceError);
procedure LoginDeviceKey(ACredentialId, AChallengeToken: string;
ASuccess: TOnLoginSuccess; AError: TOnLoginError);
......@@ -246,7 +246,7 @@ end;
// ---- WebAuthn registration ----
procedure TAuthService.BeginRegistration(APhoneNumber: string;
procedure TAuthService.BeginRegistration(APhoneNumber, ADeviceType: string;
ASuccess: TOnCodeSentSuccess; AError: TOnDeviceError);
procedure OnLoad(Response: TXDataClientResponse);
......@@ -272,12 +272,12 @@ procedure TAuthService.BeginRegistration(APhoneNumber: string;
begin
FClient.RawInvoke(
'IAuthService.BeginRegistration',
[APhoneNumber],
[APhoneNumber, ADeviceType],
@OnLoad, @OnError
);
end;
procedure TAuthService.VerifyRegistrationCode(APhoneNumber,
procedure TAuthService.VerifyRegistrationCode(APhoneNumber, ADeviceType,
AVerificationCode: string; ASuccess: TOnBeginSuccess;
AError: TOnDeviceError);
......@@ -306,12 +306,12 @@ procedure TAuthService.VerifyRegistrationCode(APhoneNumber,
begin
FClient.RawInvoke(
'IAuthService.VerifyRegistrationCode',
[APhoneNumber, AVerificationCode],
[APhoneNumber, ADeviceType, AVerificationCode],
@OnLoad, @OnError
);
end;
procedure TAuthService.CompleteRegistration(APhoneNumber, ACredentialId,
procedure TAuthService.CompleteRegistration(APhoneNumber, ADeviceType, ACredentialId,
AAttestationObject, AClientDataJSON, AChallengeToken: string;
ASuccess: TOnDeviceSuccess; AError: TOnDeviceError);
......@@ -345,7 +345,7 @@ procedure TAuthService.CompleteRegistration(APhoneNumber, ACredentialId,
begin
FClient.RawInvoke(
'IAuthService.CompleteRegistration',
[APhoneNumber, ACredentialId, AAttestationObject, AClientDataJSON, AChallengeToken],
[APhoneNumber, ADeviceType, ACredentialId, AAttestationObject, AClientDataJSON, AChallengeToken],
@OnLoad, @OnError
);
end;
......@@ -459,7 +459,7 @@ end;
// ---- Simple-key registration and login ----
procedure TAuthService.CompleteRegistrationSimple(APhoneNumber, ADeviceKey, AChallengeToken: string;
procedure TAuthService.CompleteRegistrationSimple(APhoneNumber, ADeviceType, ADeviceKey, AChallengeToken: string;
ASuccess: TOnDeviceSuccess; AError: TOnDeviceError);
procedure OnLoad(Response: TXDataClientResponse);
......@@ -490,7 +490,7 @@ procedure TAuthService.CompleteRegistrationSimple(APhoneNumber, ADeviceKey, ACha
begin
FClient.RawInvoke(
'IAuthService.CompleteRegistrationSimple',
[APhoneNumber, ADeviceKey, AChallengeToken],
[APhoneNumber, ADeviceType, ADeviceKey, AChallengeToken],
@OnLoad, @OnError
);
end;
......
......@@ -14,17 +14,17 @@ type
// Note: A supplied connection is borrowed and must outlive this store.
procedure UseConnection(AConnection: TUniConnection); virtual; abstract;
property Connection: TUniConnection read GetConnection;
function PendingDevice(PhoneNumber: string): TDataSet; virtual; abstract;
function SetVerificationCode(Code: string; PhoneNumber: string): Int64; virtual; abstract;
function ConsumeVerificationCode(PhoneNumber: string; Code: string): Int64; virtual; abstract;
function ActivateWebAuthnDevice(CredentialId: string; UserAgent: string; PublicKeyX: TBytes; PublicKeyY: TBytes; PublicKeyAlg: Integer; SignCount: Integer; PhoneNumber: string): Int64; virtual; abstract;
function PendingDevice(PhoneNumber: string; DeviceType: string): TDataSet; virtual; abstract;
function SetVerificationCode(Code: string; PhoneNumber: string; DeviceType: string): Int64; virtual; abstract;
function ConsumeVerificationCode(PhoneNumber: string; Code: string; DeviceType: string): Int64; virtual; abstract;
function ActivateWebAuthnDevice(CredentialId: string; UserAgent: string; PublicKeyX: TBytes; PublicKeyY: TBytes; PublicKeyAlg: Integer; SignCount: Integer; PhoneNumber: string; DeviceType: string): Int64; virtual; abstract;
function UnrevokedDeviceUser(CredentialId: string): TDataSet; virtual; abstract;
function ActiveDeviceUser(CredentialId: string): TDataSet; virtual; abstract;
function UnrevokedDeviceKey(CredentialId: string): TDataSet; virtual; abstract;
function UpdateSignCount(SignCount: Integer; CredentialId: string): Int64; virtual; abstract;
function SaveDeviceUser(Username: string; Agency: string; CredentialId: string): Int64; virtual; abstract;
function ActiveDeviceKey(CredentialId: string): TDataSet; virtual; abstract;
function ActivateSimpleDevice(CredentialId: string; UserAgent: string; PhoneNumber: string): Int64; virtual; abstract;
function ActivateSimpleDevice(CredentialId: string; UserAgent: string; PhoneNumber: string; DeviceType: string): Int64; virtual; abstract;
function ActiveSimpleDeviceUser(CredentialId: string): TDataSet; virtual; abstract;
function ActiveSimpleDevice(CredentialId: string): TDataSet; virtual; abstract;
function DeviceStatus(CredentialId: string): TDataSet; virtual; abstract;
......@@ -32,7 +32,7 @@ type
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 PendingPhone(PhoneNumber: string; DeviceType: string): TDataSet; virtual; abstract;
function AddPendingDevice(DeviceName: string; DeviceType: string; PhoneNumber: string): Int64; virtual; abstract;
function DeleteDevice(DeviceId: Integer): Int64; virtual; abstract;
function ReserveRedeemCode(PhoneNumber: string; out Code: string): Boolean; virtual; abstract;
......
......@@ -18,7 +18,8 @@ object OracleDeviceDatabase: TOracleDeviceDatabase
'SELECT ID, DEVICE_NAME'
'FROM DEVICE_REGISTRATIONS'
'WHERE PHONE_NUMBER = :PHONE'
' AND STATUS = ''pending''')
' AND STATUS = ''pending'''
' AND LOWER(TRIM(DEVICE_TYPE)) = :TYPE')
ReadOnly = True
Active = False
Left = 60
......@@ -26,6 +27,11 @@ object OracleDeviceDatabase: TOracleDeviceDatabase
ParamData = <
item
DataType = ftString
Name = 'TYPE'
ParamType = ptInput
end
item
DataType = ftString
Name = 'PHONE'
ParamType = ptInput
end>
......@@ -36,13 +42,19 @@ object OracleDeviceDatabase: TOracleDeviceDatabase
'UPDATE DEVICE_REGISTRATIONS'
'SET VERIFICATION_CODE = :CODE, VERIFICATION_EXPIRES_AT = SYSTIMESTAMP + INTERVAL ''5'' MINUTE'
'WHERE PHONE_NUMBER = :PHONE'
' AND STATUS = ''pending''')
' AND STATUS = ''pending'''
' AND LOWER(TRIM(DEVICE_TYPE)) = :TYPE')
Active = False
Left = 250
Top = 110
ParamData = <
item
DataType = ftString
Name = 'TYPE'
ParamType = ptInput
end
item
DataType = ftString
Name = 'CODE'
ParamType = ptInput
end
......@@ -60,13 +72,19 @@ object OracleDeviceDatabase: TOracleDeviceDatabase
'WHERE PHONE_NUMBER = :PHONE'
' AND STATUS = ''pending'''
' AND VERIFICATION_CODE = :CODE'
' AND VERIFICATION_EXPIRES_AT > SYSTIMESTAMP')
' AND VERIFICATION_EXPIRES_AT > SYSTIMESTAMP'
' AND LOWER(TRIM(DEVICE_TYPE)) = :TYPE')
Active = False
Left = 630
Top = 110
ParamData = <
item
DataType = ftString
Name = 'TYPE'
ParamType = ptInput
end
item
DataType = ftString
Name = 'PHONE'
ParamType = ptInput
end
......@@ -82,13 +100,19 @@ object OracleDeviceDatabase: TOracleDeviceDatabase
'UPDATE DEVICE_REGISTRATIONS'
'SET CREDENTIAL_ID = :CID, USER_AGENT = :AGENT, PUBLIC_KEY_X = :KEYX, PUBLIC_KEY_Y = :KEYY, PUBLIC_KEY_ALG = :ALG, SIGN_COUNT = :CNT, VERIFICATION_CODE = NULL, VERIFICATION_EXPIRES_AT = NULL, STATUS = ''active'', REGISTERED_AT = SYSTIMESTAMP'
'WHERE PHONE_NUMBER = :PHONE'
' AND STATUS = ''pending''')
' AND STATUS = ''pending'''
' AND LOWER(TRIM(DEVICE_TYPE)) = :TYPE')
Active = False
Left = 820
Top = 110
ParamData = <
item
DataType = ftString
Name = 'TYPE'
ParamType = ptInput
end
item
DataType = ftString
Name = 'CID'
ParamType = ptInput
end
......@@ -250,13 +274,19 @@ object OracleDeviceDatabase: TOracleDeviceDatabase
'UPDATE DEVICE_REGISTRATIONS'
'SET CREDENTIAL_ID = :CID, USER_AGENT = :AGENT, KEY_TYPE = ''simple'', VERIFICATION_CODE = NULL, VERIFICATION_EXPIRES_AT = NULL, STATUS = ''active'', REGISTERED_AT = SYSTIMESTAMP'
'WHERE PHONE_NUMBER = :PHONE'
' AND STATUS = ''pending''')
' AND STATUS = ''pending'''
' AND LOWER(TRIM(DEVICE_TYPE)) = :TYPE')
Active = False
Left = 250
Top = 330
ParamData = <
item
DataType = ftString
Name = 'TYPE'
ParamType = ptInput
end
item
DataType = ftString
Name = 'CID'
ParamType = ptInput
end
......@@ -398,7 +428,8 @@ object OracleDeviceDatabase: TOracleDeviceDatabase
'SELECT ID'
'FROM DEVICE_REGISTRATIONS'
'WHERE PHONE_NUMBER = :PHONE'
' AND STATUS = ''pending''')
' AND STATUS = ''pending'''
' AND LOWER(TRIM(DEVICE_TYPE)) = :TYPE')
ReadOnly = True
Active = False
Left = 630
......@@ -406,6 +437,11 @@ object OracleDeviceDatabase: TOracleDeviceDatabase
ParamData = <
item
DataType = ftString
Name = 'TYPE'
ParamType = ptInput
end
item
DataType = ftString
Name = 'PHONE'
ParamType = ptInput
end>
......
......@@ -40,17 +40,17 @@ type
function GetConnection: TUniConnection; override;
public
procedure UseConnection(AConnection: TUniConnection); override;
function PendingDevice(PhoneNumber: string): TDataSet; override;
function SetVerificationCode(Code: string; PhoneNumber: string): Int64; override;
function ConsumeVerificationCode(PhoneNumber: string; Code: string): Int64; override;
function ActivateWebAuthnDevice(CredentialId: string; UserAgent: string; PublicKeyX: TBytes; PublicKeyY: TBytes; PublicKeyAlg: Integer; SignCount: Integer; PhoneNumber: string): Int64; override;
function PendingDevice(PhoneNumber: string; DeviceType: string): TDataSet; override;
function SetVerificationCode(Code: string; PhoneNumber: string; DeviceType: string): Int64; override;
function ConsumeVerificationCode(PhoneNumber: string; Code: string; DeviceType: string): Int64; override;
function ActivateWebAuthnDevice(CredentialId: string; UserAgent: string; PublicKeyX: TBytes; PublicKeyY: TBytes; PublicKeyAlg: Integer; SignCount: Integer; PhoneNumber: string; DeviceType: string): Int64; override;
function UnrevokedDeviceUser(CredentialId: string): TDataSet; override;
function ActiveDeviceUser(CredentialId: string): TDataSet; override;
function UnrevokedDeviceKey(CredentialId: string): TDataSet; override;
function UpdateSignCount(SignCount: Integer; CredentialId: string): Int64; override;
function SaveDeviceUser(Username: string; Agency: string; CredentialId: string): Int64; override;
function ActiveDeviceKey(CredentialId: string): TDataSet; override;
function ActivateSimpleDevice(CredentialId: string; UserAgent: string; PhoneNumber: string): Int64; override;
function ActivateSimpleDevice(CredentialId: string; UserAgent: string; PhoneNumber: string; DeviceType: string): Int64; override;
function ActiveSimpleDeviceUser(CredentialId: string): TDataSet; override;
function ActiveSimpleDevice(CredentialId: string): TDataSet; override;
function DeviceStatus(CredentialId: string): TDataSet; override;
......@@ -58,7 +58,7 @@ type
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 PendingPhone(PhoneNumber: string; DeviceType: string): TDataSet; override;
function AddPendingDevice(DeviceName: string; DeviceType: string; PhoneNumber: string): Int64; override;
function DeleteDevice(DeviceId: Integer): Int64; override;
function ReserveRedeemCode(PhoneNumber: string; out Code: string): Boolean; override;
......@@ -93,33 +93,36 @@ begin
TUniQuery(Components[i]).Connection := Connection;
end;
function TOracleDeviceDatabase.PendingDevice(PhoneNumber: string): TDataSet;
function TOracleDeviceDatabase.PendingDevice(PhoneNumber: string; DeviceType: string): TDataSet;
begin
uqPendingDevice.Close;
uqPendingDevice.ParamByName('PHONE').AsString := PhoneNumber;
uqPendingDevice.ParamByName('TYPE').AsString := DeviceType;
uqPendingDevice.Open;
Result := uqPendingDevice;
end;
function TOracleDeviceDatabase.SetVerificationCode(Code: string; PhoneNumber: string): Int64;
function TOracleDeviceDatabase.SetVerificationCode(Code: string; PhoneNumber: string; DeviceType: string): Int64;
begin
uqSetVerificationCode.Close;
uqSetVerificationCode.ParamByName('CODE').AsString := Code;
uqSetVerificationCode.ParamByName('PHONE').AsString := PhoneNumber;
uqSetVerificationCode.ParamByName('TYPE').AsString := DeviceType;
uqSetVerificationCode.ExecSQL;
Result := uqSetVerificationCode.RowsAffected;
end;
function TOracleDeviceDatabase.ConsumeVerificationCode(PhoneNumber: string; Code: string): Int64;
function TOracleDeviceDatabase.ConsumeVerificationCode(PhoneNumber: string; Code: string; DeviceType: string): Int64;
begin
uqConsumeVerificationCode.Close;
uqConsumeVerificationCode.ParamByName('PHONE').AsString := PhoneNumber;
uqConsumeVerificationCode.ParamByName('TYPE').AsString := DeviceType;
uqConsumeVerificationCode.ParamByName('CODE').AsString := Code;
uqConsumeVerificationCode.ExecSQL;
Result := uqConsumeVerificationCode.RowsAffected;
end;
function TOracleDeviceDatabase.ActivateWebAuthnDevice(CredentialId: string; UserAgent: string; PublicKeyX: TBytes; PublicKeyY: TBytes; PublicKeyAlg: Integer; SignCount: Integer; PhoneNumber: string): Int64;
function TOracleDeviceDatabase.ActivateWebAuthnDevice(CredentialId: string; UserAgent: string; PublicKeyX: TBytes; PublicKeyY: TBytes; PublicKeyAlg: Integer; SignCount: Integer; PhoneNumber: string; DeviceType: string): Int64;
begin
uqActivateWebAuthnDevice.Close;
uqActivateWebAuthnDevice.ParamByName('CID').AsString := CredentialId;
......@@ -129,6 +132,7 @@ begin
uqActivateWebAuthnDevice.ParamByName('ALG').AsInteger := PublicKeyAlg;
uqActivateWebAuthnDevice.ParamByName('CNT').AsInteger := SignCount;
uqActivateWebAuthnDevice.ParamByName('PHONE').AsString := PhoneNumber;
uqActivateWebAuthnDevice.ParamByName('TYPE').AsString := DeviceType;
uqActivateWebAuthnDevice.ExecSQL;
Result := uqActivateWebAuthnDevice.RowsAffected;
end;
......@@ -184,12 +188,13 @@ begin
Result := uqActiveDeviceKey;
end;
function TOracleDeviceDatabase.ActivateSimpleDevice(CredentialId: string; UserAgent: string; PhoneNumber: string): Int64;
function TOracleDeviceDatabase.ActivateSimpleDevice(CredentialId: string; UserAgent: string; PhoneNumber: string; DeviceType: string): Int64;
begin
uqActivateSimpleDevice.Close;
uqActivateSimpleDevice.ParamByName('CID').AsString := CredentialId;
uqActivateSimpleDevice.ParamByName('AGENT').AsString := UserAgent;
uqActivateSimpleDevice.ParamByName('PHONE').AsString := PhoneNumber;
uqActivateSimpleDevice.ParamByName('TYPE').AsString := DeviceType;
uqActivateSimpleDevice.ExecSQL;
Result := uqActivateSimpleDevice.RowsAffected;
end;
......@@ -254,10 +259,11 @@ begin
end;
end;
function TOracleDeviceDatabase.PendingPhone(PhoneNumber: string): TDataSet;
function TOracleDeviceDatabase.PendingPhone(PhoneNumber: string; DeviceType: string): TDataSet;
begin
uqPendingPhone.Close;
uqPendingPhone.ParamByName('PHONE').AsString := PhoneNumber;
uqPendingPhone.ParamByName('TYPE').AsString := DeviceType;
uqPendingPhone.Open;
Result := uqPendingPhone;
end;
......
......@@ -16,12 +16,18 @@ object PostgreSQLDeviceDatabase: TPostgreSQLDeviceDatabase
SQL.Strings = (
'SELECT id, device_name FROM lems.device_registrations WHERE phon' +
'e_number = :PHONE AND status = '#39'pending'#39)
'e_number = :PHONE AND status = '#39'pending'#39
' AND LOWER(TRIM(DEVICE_TYPE)) = :TYPE')
Left = 60
Top = 110
ParamData = <
item
DataType = ftString
Name = 'TYPE'
ParamType = ptInput
end
item
DataType = ftString
Name = 'PHONE'
ParamType = ptInput
Value = nil
......@@ -33,12 +39,18 @@ object PostgreSQLDeviceDatabase: TPostgreSQLDeviceDatabase
'UPDATE lems.device_registrations SET verification_code = :CODE, ' +
' verification_expires_at = NOW() + INTERVAL '#39'5 minutes'#39' WHERE' +
' phone_number = :PHONE AND status = '#39'pending'#39)
' phone_number = :PHONE AND status = '#39'pending'#39
' AND LOWER(TRIM(DEVICE_TYPE)) = :TYPE')
Left = 188
Top = 178
ParamData = <
item
DataType = ftString
Name = 'TYPE'
ParamType = ptInput
end
item
DataType = ftString
Name = 'CODE'
ParamType = ptInput
Value = nil
......@@ -57,12 +69,18 @@ object PostgreSQLDeviceDatabase: TPostgreSQLDeviceDatabase
'UPDATE lems.device_registrations SET verification_code = NULL, v' +
'erification_expires_at = NULL WHERE phone_number = :PHONE AND st' +
'atus = '#39'pending'#39' AND verification_code = :CODE AND verification_' +
'expires_at > NOW()')
'expires_at > NOW()'
' AND LOWER(TRIM(DEVICE_TYPE)) = :TYPE')
Left = 502
Top = 130
ParamData = <
item
DataType = ftString
Name = 'TYPE'
ParamType = ptInput
end
item
DataType = ftString
Name = 'PHONE'
ParamType = ptInput
Value = nil
......@@ -83,12 +101,18 @@ object PostgreSQLDeviceDatabase: TPostgreSQLDeviceDatabase
' public_key_alg = :ALG, sign_count = :CNT, verification_c' +
'ode = NULL, verification_expires_at = NULL, status = '#39'active' +
#39', registered_at = NOW() WHERE phone_number = :PHONE AND status ' +
'= '#39'pending'#39)
'= '#39'pending'#39
' AND LOWER(TRIM(DEVICE_TYPE)) = :TYPE')
Left = 618
Top = 164
ParamData = <
item
DataType = ftString
Name = 'TYPE'
ParamType = ptInput
end
item
DataType = ftString
Name = 'CID'
ParamType = ptInput
Value = nil
......@@ -255,12 +279,18 @@ object PostgreSQLDeviceDatabase: TPostgreSQLDeviceDatabase
'agent = :AGENT, key_type = '#39'simple'#39', verification_code =' +
' NULL, verification_expires_at = NULL, status = '#39'active'#39', re' +
'gistered_at = NOW() WHERE phone_number = :PHONE AND status = '#39'pe' +
'nding'#39)
'nding'#39
' AND LOWER(TRIM(DEVICE_TYPE)) = :TYPE')
Left = 190
Top = 364
ParamData = <
item
DataType = ftString
Name = 'TYPE'
ParamType = ptInput
end
item
DataType = ftString
Name = 'CID'
ParamType = ptInput
Value = nil
......@@ -399,12 +429,18 @@ object PostgreSQLDeviceDatabase: TPostgreSQLDeviceDatabase
SQL.Strings = (
'SELECT id FROM lems.device_registrations WHERE phone_number = :P' +
'HONE AND status = '#39'pending'#39)
'HONE AND status = '#39'pending'#39
' AND LOWER(TRIM(DEVICE_TYPE)) = :TYPE')
Left = 470
Top = 458
ParamData = <
item
DataType = ftString
Name = 'TYPE'
ParamType = ptInput
end
item
DataType = ftString
Name = 'PHONE'
ParamType = ptInput
Value = nil
......
......@@ -41,17 +41,17 @@ type
function GetConnection: TUniConnection; override;
public
procedure UseConnection(AConnection: TUniConnection); override;
function PendingDevice(PhoneNumber: string): TDataSet; override;
function SetVerificationCode(Code: string; PhoneNumber: string): Int64; override;
function ConsumeVerificationCode(PhoneNumber: string; Code: string): Int64; override;
function ActivateWebAuthnDevice(CredentialId: string; UserAgent: string; PublicKeyX: TBytes; PublicKeyY: TBytes; PublicKeyAlg: Integer; SignCount: Integer; PhoneNumber: string): Int64; override;
function PendingDevice(PhoneNumber: string; DeviceType: string): TDataSet; override;
function SetVerificationCode(Code: string; PhoneNumber: string; DeviceType: string): Int64; override;
function ConsumeVerificationCode(PhoneNumber: string; Code: string; DeviceType: string): Int64; override;
function ActivateWebAuthnDevice(CredentialId: string; UserAgent: string; PublicKeyX: TBytes; PublicKeyY: TBytes; PublicKeyAlg: Integer; SignCount: Integer; PhoneNumber: string; DeviceType: string): Int64; override;
function UnrevokedDeviceUser(CredentialId: string): TDataSet; override;
function ActiveDeviceUser(CredentialId: string): TDataSet; override;
function UnrevokedDeviceKey(CredentialId: string): TDataSet; override;
function UpdateSignCount(SignCount: Integer; CredentialId: string): Int64; override;
function SaveDeviceUser(Username: string; Agency: string; CredentialId: string): Int64; override;
function ActiveDeviceKey(CredentialId: string): TDataSet; override;
function ActivateSimpleDevice(CredentialId: string; UserAgent: string; PhoneNumber: string): Int64; override;
function ActivateSimpleDevice(CredentialId: string; UserAgent: string; PhoneNumber: string; DeviceType: string): Int64; override;
function ActiveSimpleDeviceUser(CredentialId: string): TDataSet; override;
function ActiveSimpleDevice(CredentialId: string): TDataSet; override;
function DeviceStatus(CredentialId: string): TDataSet; override;
......@@ -59,7 +59,7 @@ type
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 PendingPhone(PhoneNumber: string; DeviceType: string): TDataSet; override;
function AddPendingDevice(DeviceName: string; DeviceType: string; PhoneNumber: string): Int64; override;
function DeleteDevice(DeviceId: Integer): Int64; override;
function ReserveRedeemCode(PhoneNumber: string; out Code: string): Boolean; override;
......@@ -94,33 +94,36 @@ begin
TUniQuery(Components[i]).Connection := Connection;
end;
function TPostgreSQLDeviceDatabase.PendingDevice(PhoneNumber: string): TDataSet;
function TPostgreSQLDeviceDatabase.PendingDevice(PhoneNumber: string; DeviceType: string): TDataSet;
begin
uqPendingDevice.Close;
uqPendingDevice.ParamByName('PHONE').AsString := PhoneNumber;
uqPendingDevice.ParamByName('TYPE').AsString := DeviceType;
uqPendingDevice.Open;
Result := uqPendingDevice;
end;
function TPostgreSQLDeviceDatabase.SetVerificationCode(Code: string; PhoneNumber: string): Int64;
function TPostgreSQLDeviceDatabase.SetVerificationCode(Code: string; PhoneNumber: string; DeviceType: string): Int64;
begin
uqSetVerificationCode.Close;
uqSetVerificationCode.ParamByName('CODE').AsString := Code;
uqSetVerificationCode.ParamByName('PHONE').AsString := PhoneNumber;
uqSetVerificationCode.ParamByName('TYPE').AsString := DeviceType;
uqSetVerificationCode.ExecSQL;
Result := uqSetVerificationCode.RowsAffected;
end;
function TPostgreSQLDeviceDatabase.ConsumeVerificationCode(PhoneNumber: string; Code: string): Int64;
function TPostgreSQLDeviceDatabase.ConsumeVerificationCode(PhoneNumber: string; Code: string; DeviceType: string): Int64;
begin
uqConsumeVerificationCode.Close;
uqConsumeVerificationCode.ParamByName('PHONE').AsString := PhoneNumber;
uqConsumeVerificationCode.ParamByName('TYPE').AsString := DeviceType;
uqConsumeVerificationCode.ParamByName('CODE').AsString := Code;
uqConsumeVerificationCode.ExecSQL;
Result := uqConsumeVerificationCode.RowsAffected;
end;
function TPostgreSQLDeviceDatabase.ActivateWebAuthnDevice(CredentialId: string; UserAgent: string; PublicKeyX: TBytes; PublicKeyY: TBytes; PublicKeyAlg: Integer; SignCount: Integer; PhoneNumber: string): Int64;
function TPostgreSQLDeviceDatabase.ActivateWebAuthnDevice(CredentialId: string; UserAgent: string; PublicKeyX: TBytes; PublicKeyY: TBytes; PublicKeyAlg: Integer; SignCount: Integer; PhoneNumber: string; DeviceType: string): Int64;
begin
uqActivateWebAuthnDevice.Close;
uqActivateWebAuthnDevice.ParamByName('CID').AsString := CredentialId;
......@@ -130,6 +133,7 @@ begin
uqActivateWebAuthnDevice.ParamByName('ALG').AsInteger := PublicKeyAlg;
uqActivateWebAuthnDevice.ParamByName('CNT').AsInteger := SignCount;
uqActivateWebAuthnDevice.ParamByName('PHONE').AsString := PhoneNumber;
uqActivateWebAuthnDevice.ParamByName('TYPE').AsString := DeviceType;
uqActivateWebAuthnDevice.ExecSQL;
Result := uqActivateWebAuthnDevice.RowsAffected;
end;
......@@ -185,12 +189,13 @@ begin
Result := uqActiveDeviceKey;
end;
function TPostgreSQLDeviceDatabase.ActivateSimpleDevice(CredentialId: string; UserAgent: string; PhoneNumber: string): Int64;
function TPostgreSQLDeviceDatabase.ActivateSimpleDevice(CredentialId: string; UserAgent: string; PhoneNumber: string; DeviceType: string): Int64;
begin
uqActivateSimpleDevice.Close;
uqActivateSimpleDevice.ParamByName('CID').AsString := CredentialId;
uqActivateSimpleDevice.ParamByName('AGENT').AsString := UserAgent;
uqActivateSimpleDevice.ParamByName('PHONE').AsString := PhoneNumber;
uqActivateSimpleDevice.ParamByName('TYPE').AsString := DeviceType;
uqActivateSimpleDevice.ExecSQL;
Result := uqActivateSimpleDevice.RowsAffected;
end;
......@@ -255,10 +260,11 @@ begin
end;
end;
function TPostgreSQLDeviceDatabase.PendingPhone(PhoneNumber: string): TDataSet;
function TPostgreSQLDeviceDatabase.PendingPhone(PhoneNumber: string; DeviceType: string): TDataSet;
begin
uqPendingPhone.Close;
uqPendingPhone.ParamByName('PHONE').AsString := PhoneNumber;
uqPendingPhone.ParamByName('TYPE').AsString := DeviceType;
uqPendingPhone.Open;
Result := uqPendingPhone;
end;
......
......@@ -533,13 +533,13 @@ begin
Exit;
end;
// Reject if a pending entry with this phone already exists
q := store.PendingPhone(normalizedPhone);
// Note: A phone number can have one pending registration per device type.
q := store.PendingPhone(normalizedPhone, normalizedDeviceType);
if not q.IsEmpty then
begin
q.Close;
Result.AddPair('status', 'error');
Result.AddPair('message', 'A pending entry for that phone number already exists.');
Result.AddPair('message', 'A pending entry for that phone number and device type already exists.');
Exit;
end;
q.Close;
......
......@@ -79,12 +79,12 @@ type
function VerifyVersion(ClientVersion: string): TJSONObject;
// Section: Device registration step 1 checks phone pre-auth and sends an SMS code
function BeginRegistration(const PhoneNumber: string): TJSONObject;
function BeginRegistration(const PhoneNumber, DeviceType: string): TJSONObject;
// Section: Device registration step 2 verifies the SMS code and returns a challenge
function VerifyRegistrationCode(const PhoneNumber,
function VerifyRegistrationCode(const PhoneNumber, DeviceType,
VerificationCode: string): TJSONObject;
// Section: WebAuthn registration verifies the credential and activates the pending row
function CompleteRegistration(const PhoneNumber, CredentialId,
function CompleteRegistration(const PhoneNumber, DeviceType, CredentialId,
AttestationObject, ClientDataJSON,
ChallengeToken: string): TJSONObject;
// WebAuthn authentication challenge — called before Login
......@@ -96,7 +96,7 @@ type
AuthenticatorData, ClientDataJSON,
Signature: string): string;
// Simple-key registration — client generates key, no crypto verification
function CompleteRegistrationSimple(const PhoneNumber, DeviceKey,
function CompleteRegistrationSimple(const PhoneNumber, DeviceType, DeviceKey,
ChallengeToken: string): TJSONObject;
// Simple-key auto-login — verifies challenge token + credential ownership
function LoginDeviceKey(const CredentialId, ChallengeToken: string): string;
......
......@@ -41,16 +41,16 @@ type
destructor Destroy; override;
function GetAgencieslist(): TAgenciesList;
function GetAgencyConfiglist: TAgencyConfigList;
function BeginRegistration(const PhoneNumber: string): TJSONObject;
function VerifyRegistrationCode(const PhoneNumber,
function BeginRegistration(const PhoneNumber, DeviceType: string): TJSONObject;
function VerifyRegistrationCode(const PhoneNumber, DeviceType,
VerificationCode: string): TJSONObject;
function CompleteRegistration(const PhoneNumber, CredentialId, AttestationObject, ClientDataJSON, ChallengeToken: string): TJSONObject;
function CompleteRegistration(const PhoneNumber, DeviceType, CredentialId, AttestationObject, ClientDataJSON, ChallengeToken: string): TJSONObject;
function BeginAuthentication(const CredentialId: string): TJSONObject;
function GetDeviceUser(const CredentialId: string): TJSONObject;
function LoginAutomatic(const CredentialId, ChallengeToken,
AuthenticatorData, ClientDataJSON,
Signature: string): string;
function CompleteRegistrationSimple(const PhoneNumber, DeviceKey,
function CompleteRegistrationSimple(const PhoneNumber, DeviceType, DeviceKey,
ChallengeToken: string): TJSONObject;
function LoginDeviceKey(const CredentialId, ChallengeToken: string): string;
function LoginPasswordAndSimpleKey(const User, Password, Agency,
......@@ -257,7 +257,7 @@ end;
// BeginRegistration
// ---------------------------------------------------------------------------
function TAuthService.BeginRegistration(const PhoneNumber: string): TJSONObject;
function TAuthService.BeginRegistration(const PhoneNumber, DeviceType: string): TJSONObject;
var
affectedRows: Int64;
code, normalizedPhone, deviceName: string;
......@@ -287,14 +287,24 @@ begin
end;
end;
if (not SameText(Trim(DeviceType), 'iphone')) and
(not SameText(Trim(DeviceType), 'android')) and
(not SameText(Trim(DeviceType), 'browser')) and
(not SameText(Trim(DeviceType), 'other')) then
begin
Result.AddPair('status', 'error');
Result.AddPair('message', 'Please select a valid device type.');
Exit;
end;
// Verify phone number was pre-authorized by admin
q := FDeviceStore.PendingDevice(normalizedPhone);
q := FDeviceStore.PendingDevice(normalizedPhone, LowerCase(Trim(DeviceType)));
if q.IsEmpty then
begin
q.Close;
Logger.Log(2, 'BeginRegistration - phone not pre-authorized: "' + normalizedPhone + '"');
Result.AddPair('status', 'error');
Result.AddPair('message', 'Phone number not recognized. Contact your administrator.');
Result.AddPair('message', 'No pending registration matches that phone number and device type. Contact your administrator.');
Exit;
end;
deviceId := q.FieldByName('id').AsInteger;
......@@ -320,11 +330,11 @@ begin
end;
// Note: Persist the code only after the SMS provider accepts the message.
affectedRows := FDeviceStore.SetVerificationCode(code, normalizedPhone);
affectedRows := FDeviceStore.SetVerificationCode(code, normalizedPhone, LowerCase(Trim(DeviceType)));
if affectedRows = 0 then
begin
Result.AddPair('status', 'error');
Result.AddPair('message', 'Phone number not recognized. Contact your administrator.');
Result.AddPair('message', 'No pending registration matches that phone number and device type. Contact your administrator.');
Exit;
end;
......@@ -336,7 +346,7 @@ begin
Logger.Log(3, '---TAuthService.BeginRegistration End');
end;
function TAuthService.VerifyRegistrationCode(const PhoneNumber,
function TAuthService.VerifyRegistrationCode(const PhoneNumber, DeviceType,
VerificationCode: string): TJSONObject;
var
affectedRows: Int64;
......@@ -360,7 +370,7 @@ begin
verificationValid := False;
if Length(Trim(VerificationCode)) = 6 then
begin
affectedRows := FDeviceStore.ConsumeVerificationCode(normalizedPhone, Trim(VerificationCode));
affectedRows := FDeviceStore.ConsumeVerificationCode(normalizedPhone, Trim(VerificationCode), LowerCase(Trim(DeviceType)));
verificationValid := affectedRows > 0;
end;
......@@ -373,7 +383,7 @@ begin
Exit;
end;
token := MakeChallengeToken('reg-' + normalizedPhone);
token := MakeChallengeToken('reg-' + normalizedPhone + '-' + LowerCase(Trim(DeviceType)));
challengeB64 := token.Split([':'], 4)[0];
Result.AddPair('status', 'ok');
......@@ -389,7 +399,7 @@ end;
// CompleteRegistration
// ---------------------------------------------------------------------------
function TAuthService.CompleteRegistration(const PhoneNumber, CredentialId,
function TAuthService.CompleteRegistration(const PhoneNumber, DeviceType, CredentialId,
AttestationObject, ClientDataJSON, ChallengeToken: string): TJSONObject;
var
affectedRows: Int64;
......@@ -424,7 +434,7 @@ begin
// 1. Verify challenge token
if not VerifyChallengeToken(ChallengeToken,
'reg-' + normalizedPhone, challengeB64) then
'reg-' + normalizedPhone + '-' + LowerCase(Trim(DeviceType)), challengeB64) then
begin
Result.AddPair('status', 'error');
Result.AddPair('message', 'Invalid or expired registration challenge.');
......@@ -547,7 +557,7 @@ begin
if ctx <> nil then
userAgent := ctx.Request.Headers.Get('User-Agent');
affectedRows := FDeviceStore.ActivateWebAuthnDevice(Trim(CredentialId), userAgent, pubKeyX, pubKeyY, pubKeyAlg, Integer(signCount), normalizedPhone);
affectedRows := FDeviceStore.ActivateWebAuthnDevice(Trim(CredentialId), userAgent, pubKeyX, pubKeyY, pubKeyAlg, Integer(signCount), normalizedPhone, LowerCase(Trim(DeviceType)));
if affectedRows = 0 then
begin
......@@ -998,7 +1008,7 @@ end;
// CompleteRegistrationSimple — client-generated key, no crypto verification
// ---------------------------------------------------------------------------
function TAuthService.CompleteRegistrationSimple(const PhoneNumber, DeviceKey,
function TAuthService.CompleteRegistrationSimple(const PhoneNumber, DeviceType, DeviceKey,
ChallengeToken: string): TJSONObject;
var
affectedRows: Int64;
......@@ -1021,7 +1031,7 @@ begin
end;
if not VerifyChallengeToken(ChallengeToken,
'reg-' + normalizedPhone, challengeB64) then
'reg-' + normalizedPhone + '-' + LowerCase(Trim(DeviceType)), challengeB64) then
begin
Result.AddPair('status', 'error');
Result.AddPair('message', 'Invalid or expired registration challenge.');
......@@ -1040,7 +1050,7 @@ begin
if ctx <> nil then
userAgent := ctx.Request.Headers.Get('User-Agent');
affectedRows := FDeviceStore.ActivateSimpleDevice(Trim(DeviceKey), userAgent, normalizedPhone);
affectedRows := FDeviceStore.ActivateSimpleDevice(Trim(DeviceKey), userAgent, normalizedPhone, LowerCase(Trim(DeviceType)));
if affectedRows = 0 then
begin
......
......@@ -18,6 +18,19 @@ object FViewDeviceRegistration: TFViewDeviceRegistration
TextHint = '(303) 555-1234'
WidthPercent = 100.000000000000000000
end
object cmbDeviceType: TWebComboBox
Left = 240
Top = 110
Width = 177
Height = 25
ElementID = 'view.devicereg.devicetype'
ItemIndex = -1
Items.Strings = (
'iPhone'
'Android'
'Browser'
'Other')
end
object btnRegister: TWebButton
Left = 240
Top = 163
......
......@@ -33,7 +33,7 @@
<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,
Enter the phone number and select the device type your administrator registered for this device,
then click <strong>Send Code</strong>.
</p>
......@@ -46,6 +46,11 @@
autofocus>
</div>
<div class="mb-3">
<label class="form-label small text-muted" for="view.devicereg.devicetype">Device type</label>
<select id="view.devicereg.devicetype" class="form-select" aria-label="Device type"></select>
</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"
......
......@@ -11,6 +11,7 @@ uses
type
TFViewDeviceRegistration = class(TWebForm)
edtPhoneNumber: TWebEdit;
cmbDeviceType: TWebComboBox;
btnRegister: TWebButton;
pnlVerification: TWebPanel;
edtVerificationCode: TWebEdit;
......@@ -25,6 +26,7 @@ type
procedure WebFormCreate(Sender: TObject);
private
FRegistrationProc: TSuccessProc;
FDeviceType: string;
procedure ShowNotification(const AMsg: string; AIsError: Boolean = True);
procedure HideNotification;
procedure SetBusy(ABusy: Boolean);
......@@ -90,6 +92,7 @@ end;
procedure TFViewDeviceRegistration.SetBusy(ABusy: Boolean);
begin
cmbDeviceType.Enabled := (not ABusy) and edtPhoneNumber.Enabled;
asm
var btn = document.getElementById('view.devicereg.btnregister');
if (btn) {
......@@ -118,6 +121,7 @@ var
begin
SetBusy(False);
edtPhoneNumber.Enabled := False;
cmbDeviceType.Enabled := False;
pnlVerification.ElementHandle.classList.remove('d-none');
edtVerificationCode.SetFocus;
ShowNotification('Verification code sent. Enter the six-digit code to continue.', False);
......@@ -137,10 +141,17 @@ begin
Exit;
end;
if cmbDeviceType.ItemIndex < 0 then
begin
ShowNotification('Please select the device type your administrator registered.');
Exit;
end;
FDeviceType := LowerCase(cmbDeviceType.Text);
SetBusy(True);
HideNotification;
AuthService.BeginRegistration(phoneNumber, @OnCodeSent, @OnCodeError);
AuthService.BeginRegistration(phoneNumber, FDeviceType, @OnCodeSent, @OnCodeError);
end;
procedure TFViewDeviceRegistration.btnVerifyCodeClick(Sender: TObject);
......@@ -178,7 +189,7 @@ begin
SetVerifyBusy(True);
HideNotification;
AuthService.VerifyRegistrationCode(phoneNumber, verificationCode,
AuthService.VerifyRegistrationCode(phoneNumber, FDeviceType, verificationCode,
@OnVerified, @OnVerifyError);
end;
......@@ -201,7 +212,7 @@ var
procedure OnCredential(ACredentialId, AAttestationObject, AClientDataJSON: string);
begin
AuthService.CompleteRegistration(
phoneNumber, ACredentialId, AAttestationObject, AClientDataJSON, challengeToken,
phoneNumber, FDeviceType, ACredentialId, AAttestationObject, AClientDataJSON, challengeToken,
@OnCompleteOK, @OnCompleteError
);
end;
......@@ -295,7 +306,7 @@ begin
end;
AuthService.CompleteRegistrationSimple(
phoneNumber, deviceKey, challengeToken,
phoneNumber, FDeviceType, deviceKey, challengeToken,
@OnCompleteOK, @OnCompleteError
);
end;
......
......@@ -2,7 +2,7 @@
Client=WC
[Settings]
LogFileNum=184
LogFileNum=185
webClientVersion=0.9.4.9
MemoLogLevel=3
FileLogLevel=4
......
emiMobile_BFD @ 13d204d6
Subproject commit 13d204d656b7ccc69148d12d46d95e642e8b0a44
unit DTOGeneratorUtils;
interface
uses
SysUtils, Classes, Uni, UniProvider, DB, System.JSON, Pkg.Json.DTO;
procedure GenerateDelphiJDOForTable(UniConnection: TUniConnection; const TableName: string; S: TStrings);
function GenerateJsonDTOFromJson(const JsonStr, ClassName: string): string;
function JsonTypeToDelphiType(const Value: TJSONValue): string;
function SqlTypeToDelphiType(const SqlType: string): string;
procedure GenerateJDOsForTables(UniConn: TUniConnection);
procedure JsonToDTO(JsonStr:string);
implementation
function JsonTypeToDelphiType(const Value: TJSONValue): string;
begin
if Value is TJSONNumber then
begin
if Pos('.', Value.Value) > 0 then
Result := 'Double'
else
Result := 'Integer'
end
else if Value is TJSONString then
Result := 'string'
else if Value is TJSONBool then
Result := 'Boolean'
else if Value is TJSONArray then
Result := 'TArray<string>' // Or further analysis could detect array of objects/numbers/etc
else if Value is TJSONObject then
Result := 'TObject' // Extend for nested DTOs if needed
else if Value is TJSONNull then
Result := 'Variant'
else
Result := 'string'
end;
function SqlTypeToDelphiType(const SqlType: string): string;
begin
if SqlType.Contains('int') then
Result := 'Integer'
else if SqlType.Contains('char') or SqlType.Contains('text') then
Result := 'string'
else if SqlType.Contains('bool') then
Result := 'Boolean'
else if SqlType.Contains('date') or SqlType.Contains('time') then
Result := 'TDateTime'
else if SqlType.Contains('double') or SqlType.Contains('numeric') or SqlType.Contains('real') then
Result := 'Double'
else
Result := 'string'; // fallback
end;
//utility
function PascalCase(const S: string): string;
var
i: Integer;
NextUpper: Boolean;
begin
Result := '';
NextUpper := True;
for i := 1 to Length(S) do
begin
if S[i] in ['a'..'z', 'A'..'Z', '0'..'9'] then
begin
if NextUpper then
Result := Result + UpCase(S[i])
else
Result := Result + LowerCase(S[i]);
NextUpper := False;
end
else
NextUpper := True;
end;
end;
procedure GenerateDelphiJDOForTable(UniConnection: TUniConnection; const TableName: string; S: TStrings);
var
Q: TUniQuery;
FieldName, FieldType, DelphiType: string;
begin
Q := TUniQuery.Create(nil);
try
Q.Connection := UniConnection;
Q.SQL.Text := Format(
'SELECT column_name, data_type ' +
'FROM information_schema.columns ' +
'WHERE table_name = %s ORDER BY ordinal_position', [QuotedStr(TableName)]);
Q.Open;
S.Add('type');
S.Add(' T' + TableName + 'JDO = class');
S.Add(' public');
while not Q.Eof do
begin
FieldName := Q.FieldByName('column_name').AsString;
FieldType := Q.FieldByName('data_type').AsString;
DelphiType := SqlTypeToDelphiType(FieldType);
// PascalCase the field
FieldName := StringReplace(FieldName, '_', '', [rfReplaceAll]);
FieldName[1] := UpCase(FieldName[1]);
S.Add(Format(' %s: %s;', [FieldName, DelphiType]));
Q.Next;
end;
S.Add(' end;');
S.Add('');
finally
Q.Free;
end;
end;
procedure GenerateJDOsForTables(UniConn: TUniConnection);
var
TableList: TArray<string>;
Table: string;
OutCode: TStringList;
begin
TableList := ['customer', 'orders', 'product']; // put your schema table names here
OutCode := TStringList.Create;
try
for Table in TableList do
begin
OutCode.Clear();
GenerateDelphiJDOForTable(UniConn, Table, OutCode);
// Output to console, file, or in-memory
OutCode.SaveToFile(Table+'JDOs.pas');
//Writeln(OutCode.Text);
end;
finally
OutCode.Free;
UniConn.Free;
end;
end;
function GenerateJsonDTOFromJson(const JsonStr, ClassName: string): string;
var
Json: TJSONObject;
JPair: TJSONPair;
FieldType, PubPropName, PriFieldName: string;
Code: TStringList;
begin
Code := TStringList.Create;
try
Json := TJSONObject.ParseJSONValue(JsonStr) as TJSONObject;
if not Assigned(Json) then
raise Exception.Create('Invalid/Unsupported JSON root object.');
Code.Add('type');
Code.Add(' T' + PascalCase(ClassName) + 'DTO = class(TJsonDTO)');
Code.Add(' private');
// Private fields
for JPair in Json do
begin
FieldType := JsonTypeToDelphiType(JPair.JsonValue);
PriFieldName := 'F' + PascalCase(JPair.JsonString.Value);
Code.Add(Format(' %s: %s;', [PriFieldName, FieldType]));
end;
Code.Add(' published');
// Published properties (JSON mapping)
for JPair in Json do
begin
FieldType := JsonTypeToDelphiType(JPair.JsonValue);
PubPropName := JPair.JsonString.Value; // Use original JSON key for property
PriFieldName := 'F' + PascalCase(PubPropName);
Code.Add(Format(' property %s: %s read %s write %s;', [
PubPropName, FieldType, PriFieldName, PriFieldName
]));
end;
Code.Add(' end;');
Result := Code.Text;
Json.Free;
finally
Code.Free;
end;
end;
procedure JsonToDTO(JsonStr:string);
var
ClassName: string;
ClassSource: string;
OutCode: TStringList;
begin
OutCode := TStringList.Create;
try
ClassName:='Customer';
JsonStr := '{ "id": 1, "name": "Alice", "isActive": true, "score": 42.1, "tags": ["one", "two"] }';
ClassSource := GenerateJsonDTOFromJson(JsonStr, ClassName);
Writeln(ClassSource);
OutCode.SaveToFile(ClassName+'JDOs.pas');
//Writeln(OutCode.Text);
finally
OutCode.Free
end;
end;
end.
# Development tools
DTOGeneratorUtils.pas is a retained Delphi source-generation utility. It is not part of the emiMobile server build. It uses UniDAC and Pkg.Json.DTO; this is reference tooling, not a standalone executable project.
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