Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
emiMobile
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Mac Stephens
emiMobile
Commits
886d9a67
Commit
886d9a67
authored
Sep 10, 2026
by
Michael Brachmann
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
updates for auto login after passkey
parent
cf24b2b6
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
360 additions
and
31 deletions
+360
-31
Api.Service.pas
emiMobileServer/Source/Api.Service.pas
+1
-0
Api.ServiceImpl.pas
emiMobileServer/Source/Api.ServiceImpl.pas
+52
-2
Auth.Service.pas
emiMobileServer/Source/Auth.Service.pas
+8
-0
Auth.ServiceImpl.pas
emiMobileServer/Source/Auth.ServiceImpl.pas
+0
-0
device_registrations_username.sql
emiMobileServer/device_registrations_username.sql
+8
-0
Auth.Service.pas
webEMIMobile/Auth.Service.pas
+54
-3
View.DeviceManager.html
webEMIMobile/View.DeviceManager.html
+2
-1
View.DeviceManager.pas
webEMIMobile/View.DeviceManager.pas
+53
-4
View.Login.html
webEMIMobile/View.Login.html
+38
-20
View.Login.pas
webEMIMobile/View.Login.pas
+144
-1
No files found.
emiMobileServer/Source/Api.Service.pas
View file @
886d9a67
...
@@ -37,6 +37,7 @@ type
...
@@ -37,6 +37,7 @@ type
function
AddPendingDevice
(
const
DeviceName
,
PhoneNumber
:
string
):
TJSONObject
;
function
AddPendingDevice
(
const
DeviceName
,
PhoneNumber
:
string
):
TJSONObject
;
function
DeletePendingDevice
(
const
PhoneNumber
:
string
):
TJSONObject
;
function
DeletePendingDevice
(
const
PhoneNumber
:
string
):
TJSONObject
;
function
SendAppLink
(
const
PhoneNumber
:
string
):
TJSONObject
;
function
SendAppLink
(
const
PhoneNumber
:
string
):
TJSONObject
;
function
UpdateDeviceUsername
(
const
CredentialId
,
Username
:
string
):
TJSONObject
;
end
;
end
;
implementation
implementation
...
...
emiMobileServer/Source/Api.ServiceImpl.pas
View file @
886d9a67
...
@@ -43,6 +43,7 @@ type
...
@@ -43,6 +43,7 @@ type
function
AddPendingDevice
(
const
DeviceName
,
PhoneNumber
:
string
):
TJSONObject
;
function
AddPendingDevice
(
const
DeviceName
,
PhoneNumber
:
string
):
TJSONObject
;
function
DeletePendingDevice
(
const
PhoneNumber
:
string
):
TJSONObject
;
function
DeletePendingDevice
(
const
PhoneNumber
:
string
):
TJSONObject
;
function
SendAppLink
(
const
PhoneNumber
:
string
):
TJSONObject
;
function
SendAppLink
(
const
PhoneNumber
:
string
):
TJSONObject
;
function
UpdateDeviceUsername
(
const
CredentialId
,
Username
:
string
):
TJSONObject
;
end
;
end
;
implementation
implementation
...
@@ -1346,8 +1347,8 @@ begin
...
@@ -1346,8 +1347,8 @@ begin
try
try
q
.
Connection
:=
conn
;
q
.
Connection
:=
conn
;
q
.
SQL
.
Text
:=
q
.
SQL
.
Text
:=
'SELECT id, credential_id, device_name, phone_number, user
_agent
, '
+
'SELECT id, credential_id, device_name, phone_number, user
name
, '
+
' registered_at, revoked_at, revoked_by, status '
+
'
user_agent,
registered_at, revoked_at, revoked_by, status '
+
'FROM lems.device_registrations '
+
'FROM lems.device_registrations '
+
'ORDER BY registered_at DESC'
;
'ORDER BY registered_at DESC'
;
q
.
Open
;
q
.
Open
;
...
@@ -1361,6 +1362,7 @@ begin
...
@@ -1361,6 +1362,7 @@ begin
item
.
credential_id
:=
q
.
FieldByName
(
'credential_id'
).
AsString
;
item
.
credential_id
:=
q
.
FieldByName
(
'credential_id'
).
AsString
;
item
.
device_name
:=
q
.
FieldByName
(
'device_name'
).
AsString
;
item
.
device_name
:=
q
.
FieldByName
(
'device_name'
).
AsString
;
item
.
phone_number
:=
q
.
FieldByName
(
'phone_number'
).
AsString
;
item
.
phone_number
:=
q
.
FieldByName
(
'phone_number'
).
AsString
;
item
.
username
:=
q
.
FieldByName
(
'username'
).
AsString
;
item
.
user_agent
:=
q
.
FieldByName
(
'user_agent'
).
AsString
;
item
.
user_agent
:=
q
.
FieldByName
(
'user_agent'
).
AsString
;
item
.
registered_at
:=
q
.
FieldByName
(
'registered_at'
).
AsString
;
item
.
registered_at
:=
q
.
FieldByName
(
'registered_at'
).
AsString
;
if
q
.
FieldByName
(
'revoked_at'
).
IsNull
then
if
q
.
FieldByName
(
'revoked_at'
).
IsNull
then
...
@@ -1725,6 +1727,54 @@ begin
...
@@ -1725,6 +1727,54 @@ begin
Result
.
AddPair
(
'code'
,
redeemCode
);
Result
.
AddPair
(
'code'
,
redeemCode
);
end
;
end
;
function
TApiService
.
UpdateDeviceUsername
(
const
CredentialId
,
Username
:
string
):
TJSONObject
;
var
conn
:
TUniConnection
;
q
:
TUniQuery
;
begin
RequireAdmin
;
Result
:=
TJSONObject
.
Create
;
TXDataOperationContext
.
Current
.
Handler
.
ManagedObjects
.
Add
(
Result
);
if
Trim
(
CredentialId
)
=
''
then
begin
Result
.
AddPair
(
'status'
,
'error'
);
Result
.
AddPair
(
'message'
,
'CredentialId is required.'
);
Exit
;
end
;
conn
:=
OpenLemsConnection
;
try
q
:=
TUniQuery
.
Create
(
nil
);
try
q
.
Connection
:=
conn
;
q
.
SQL
.
Text
:=
'UPDATE lems.device_registrations '
+
'SET username = :UNAME '
+
'WHERE credential_id = :CID AND status = ''active'''
;
q
.
ParamByName
(
'UNAME'
).
AsString
:=
Trim
(
Username
);
q
.
ParamByName
(
'CID'
).
AsString
:=
Trim
(
CredentialId
);
q
.
ExecSQL
;
if
q
.
RowsAffected
=
0
then
begin
Result
.
AddPair
(
'status'
,
'error'
);
Result
.
AddPair
(
'message'
,
'Device not found or not active.'
);
Exit
;
end
;
finally
q
.
Free
;
end
;
finally
conn
.
Free
;
end
;
Logger
.
Log
(
2
,
Format
(
'TApiService.UpdateDeviceUsername - set username "%s" on credId %s'
,
[
Trim
(
Username
),
Copy
(
CredentialId
,
1
,
20
)]));
Result
.
AddPair
(
'status'
,
'ok'
);
end
;
initialization
initialization
RegisterServiceType
(
TApiService
);
RegisterServiceType
(
TApiService
);
...
...
emiMobileServer/Source/Auth.Service.pas
View file @
886d9a67
...
@@ -46,6 +46,8 @@ type
...
@@ -46,6 +46,8 @@ type
credential_id
:
string
;
credential_id
:
string
;
device_name
:
string
;
device_name
:
string
;
phone_number
:
string
;
phone_number
:
string
;
username
:
string
;
agency
:
string
;
user_agent
:
string
;
user_agent
:
string
;
registered_at
:
string
;
registered_at
:
string
;
revoked_at
:
string
;
revoked_at
:
string
;
...
@@ -80,6 +82,12 @@ type
...
@@ -80,6 +82,12 @@ type
ChallengeToken
:
string
):
TJSONObject
;
ChallengeToken
:
string
):
TJSONObject
;
// WebAuthn authentication challenge — called before Login
// WebAuthn authentication challenge — called before Login
function
BeginAuthentication
(
const
CredentialId
:
string
):
TJSONObject
;
function
BeginAuthentication
(
const
CredentialId
:
string
):
TJSONObject
;
// Returns stored username/agency for a credential (unauthenticated; used by login form)
function
GetDeviceUser
(
const
CredentialId
:
string
):
TJSONObject
;
// Passkey-only login (no password) — uses stored username+agency from device record
function
LoginAutomatic
(
const
CredentialId
,
ChallengeToken
,
AuthenticatorData
,
ClientDataJSON
,
Signature
:
string
):
string
;
end
;
end
;
implementation
implementation
...
...
emiMobileServer/Source/Auth.ServiceImpl.pas
View file @
886d9a67
This diff is collapsed.
Click to expand it.
emiMobileServer/device_registrations_username.sql
0 → 100644
View file @
886d9a67
-- Migration: add username and agency to device_registrations
-- username: the CAD username of the person who last logged in from this device
-- (saved automatically on first successful login, or set manually by admin)
-- agency: the CAD agency used at login time (saved automatically; required for auto-login)
ALTER
TABLE
lems
.
device_registrations
ADD
COLUMN
IF
NOT
EXISTS
username
VARCHAR
(
50
),
ADD
COLUMN
IF
NOT
EXISTS
agency
VARCHAR
(
20
);
webEMIMobile/Auth.Service.pas
View file @
886d9a67
...
@@ -14,9 +14,10 @@ type
...
@@ -14,9 +14,10 @@ type
TOnLoginSuccess
=
reference
to
procedure
;
TOnLoginSuccess
=
reference
to
procedure
;
TOnLoginError
=
reference
to
procedure
(
AMsg
:
string
);
TOnLoginError
=
reference
to
procedure
(
AMsg
:
string
);
TOnBeginSuccess
=
reference
to
procedure
(
AChallenge
,
AChallengeToken
:
string
);
TOnBeginSuccess
=
reference
to
procedure
(
AChallenge
,
AChallengeToken
:
string
);
TOnDeviceSuccess
=
reference
to
procedure
;
TOnDeviceSuccess
=
reference
to
procedure
;
TOnDeviceError
=
reference
to
procedure
(
AMsg
:
string
);
TOnDeviceError
=
reference
to
procedure
(
AMsg
:
string
);
TOnGetDeviceUserOK
=
reference
to
procedure
(
AUsername
,
AAgency
:
string
);
TAuthService
=
class
TAuthService
=
class
private
private
...
@@ -54,6 +55,11 @@ type
...
@@ -54,6 +55,11 @@ type
procedure
LoginWithAssertion
(
AUser
,
APassword
,
AAgency
,
ACredentialId
,
procedure
LoginWithAssertion
(
AUser
,
APassword
,
AAgency
,
ACredentialId
,
AChallengeToken
,
AAuthenticatorData
,
AClientDataJSON
,
ASignature
:
string
;
AChallengeToken
,
AAuthenticatorData
,
AClientDataJSON
,
ASignature
:
string
;
ASuccess
:
TOnLoginSuccess
;
AError
:
TOnLoginError
);
ASuccess
:
TOnLoginSuccess
;
AError
:
TOnLoginError
);
// Passkey-only auto-login (no password) — uses stored username on device record
procedure
GetDeviceUser
(
ACredentialId
:
string
;
ASuccess
:
TOnGetDeviceUserOK
);
procedure
LoginAutomatic
(
ACredentialId
,
AChallengeToken
,
AAuthenticatorData
,
AClientDataJSON
,
ASignature
:
string
;
ASuccess
:
TOnLoginSuccess
;
AError
:
TOnLoginError
);
end
;
end
;
TJwtHelper
=
class
TJwtHelper
=
class
...
@@ -282,6 +288,51 @@ begin
...
@@ -282,6 +288,51 @@ begin
);
);
end
;
end
;
// ---- Passkey auto-login ----
procedure
TAuthService
.
GetDeviceUser
(
ACredentialId
:
string
;
ASuccess
:
TOnGetDeviceUserOK
);
procedure
OnLoad
(
Response
:
TXDataClientResponse
);
var
resp
:
JS
.
TJSObject
;
begin
resp
:=
JS
.
TJSObject
(
Response
.
Result
);
ASuccess
(
JS
.
toString
(
resp
.
Properties
[
'username'
]),
JS
.
toString
(
resp
.
Properties
[
'agency'
])
);
end
;
begin
FClient
.
RawInvoke
(
'IAuthService.GetDeviceUser'
,
[
ACredentialId
],
@
OnLoad
);
end
;
procedure
TAuthService
.
LoginAutomatic
(
ACredentialId
,
AChallengeToken
,
AAuthenticatorData
,
AClientDataJSON
,
ASignature
:
string
;
ASuccess
:
TOnLoginSuccess
;
AError
:
TOnLoginError
);
procedure
OnLoad
(
Response
:
TXDataClientResponse
);
var
Token
:
JS
.
TJSObject
;
begin
Token
:=
JS
.
TJSObject
(
Response
.
Result
);
SetToken
(
JS
.
toString
(
Token
.
Properties
[
'value'
]));
ASuccess
;
end
;
procedure
OnError
(
Error
:
TXDataClientError
);
begin
AError
(
Format
(
'%s: %s'
,
[
Error
.
ErrorCode
,
Error
.
ErrorMessage
]));
end
;
begin
FClient
.
RawInvoke
(
'IAuthService.LoginAutomatic'
,
[
ACredentialId
,
AChallengeToken
,
AAuthenticatorData
,
AClientDataJSON
,
ASignature
],
@
OnLoad
,
@
OnError
);
end
;
// ---- Token helpers ----
// ---- Token helpers ----
function
TAuthService
.
TokenExpirationDate
:
TDateTime
;
function
TAuthService
.
TokenExpirationDate
:
TDateTime
;
...
...
webEMIMobile/View.DeviceManager.html
View file @
886d9a67
...
@@ -48,10 +48,11 @@
...
@@ -48,10 +48,11 @@
<tr>
<tr>
<th
style=
"min-width:130px;"
>
Device Name
</th>
<th
style=
"min-width:130px;"
>
Device Name
</th>
<th
style=
"min-width:120px;"
>
Phone
</th>
<th
style=
"min-width:120px;"
>
Phone
</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>
<th
style=
"min-width:80px;"
>
Status
</th>
<th
style=
"min-width:80px;"
>
Status
</th>
<th
style=
"min-width:1
6
0px;"
>
Actions
</th>
<th
style=
"min-width:1
8
0px;"
>
Actions
</th>
</tr>
</tr>
</thead>
</thead>
<tbody
id=
"view.devmgr.tbody"
>
<tbody
id=
"view.devmgr.tbody"
>
...
...
webEMIMobile/View.DeviceManager.pas
View file @
886d9a67
...
@@ -23,13 +23,14 @@ type
...
@@ -23,13 +23,14 @@ type
procedure
ShowNotification
(
const
AMsg
:
string
;
AIsError
:
Boolean
=
True
);
procedure
ShowNotification
(
const
AMsg
:
string
;
AIsError
:
Boolean
=
True
);
procedure
HideNotification
;
procedure
HideNotification
;
procedure
ClearTable
;
procedure
ClearTable
;
procedure
AddDeviceRow
(
const
ACredentialId
,
AName
,
APhoneNumber
,
procedure
AddDeviceRow
(
const
ACredentialId
,
AName
,
APhoneNumber
,
AUsername
,
AUserAgent
,
ARegisteredAt
,
AStatus
:
string
);
AUserAgent
,
ARegisteredAt
,
AStatus
:
string
);
[
async
]
procedure
LoadDevices
;
[
async
]
procedure
LoadDevices
;
[
async
]
procedure
RevokeDevice
(
const
ACredentialId
,
AName
:
string
);
[
async
]
procedure
RevokeDevice
(
const
ACredentialId
,
AName
:
string
);
[
async
]
procedure
DeletePendingDevice
(
const
APhoneNumber
,
AName
:
string
);
[
async
]
procedure
DeletePendingDevice
(
const
APhoneNumber
,
AName
:
string
);
[
async
]
procedure
SendAppLink
(
const
APhoneNumber
,
AName
:
string
);
[
async
]
procedure
SendAppLink
(
const
APhoneNumber
,
AName
:
string
);
[
async
]
procedure
AddPendingDevice
;
[
async
]
procedure
AddPendingDevice
;
[
async
]
procedure
UpdateDeviceUsername
(
const
ACredentialId
,
AUsername
:
string
);
public
public
end
;
end
;
...
@@ -116,18 +117,19 @@ begin
...
@@ -116,18 +117,19 @@ begin
end
;
end
;
procedure
TFViewDeviceManager
.
AddDeviceRow
(
const
ACredentialId
,
AName
,
procedure
TFViewDeviceManager
.
AddDeviceRow
(
const
ACredentialId
,
AName
,
APhoneNumber
,
AUserAgent
,
ARegisteredAt
,
AStatus
:
string
);
APhoneNumber
,
AUser
name
,
AUser
Agent
,
ARegisteredAt
,
AStatus
:
string
);
var
var
tbody
,
tr
,
tdName
,
tdPhone
,
tdAgent
,
tdReg
,
tdStatus
,
tdAction
:
TJSHTMLElement
;
tbody
,
tr
,
tdName
,
tdPhone
,
td
User
,
td
Agent
,
tdReg
,
tdStatus
,
tdAction
:
TJSHTMLElement
;
btn
,
btnSend
:
TJSHTMLElement
;
btn
,
btnSend
:
TJSHTMLElement
;
displayDate
,
displayPhone
:
string
;
displayDate
,
displayPhone
:
string
;
isPending
,
isRevoked
:
Boolean
;
isPending
,
isRevoked
,
isActive
:
Boolean
;
begin
begin
tbody
:=
TJSHTMLElement
(
document
.
getElementById
(
'view.devmgr.tbody'
));
tbody
:=
TJSHTMLElement
(
document
.
getElementById
(
'view.devmgr.tbody'
));
if
not
Assigned
(
tbody
)
then
Exit
;
if
not
Assigned
(
tbody
)
then
Exit
;
isPending
:=
AStatus
=
'pending'
;
isPending
:=
AStatus
=
'pending'
;
isRevoked
:=
AStatus
=
'revoked'
;
isRevoked
:=
AStatus
=
'revoked'
;
isActive
:=
AStatus
=
'active'
;
displayPhone
:=
FormatPhoneDisplay
(
APhoneNumber
);
displayPhone
:=
FormatPhoneDisplay
(
APhoneNumber
);
tr
:=
TJSHTMLElement
(
document
.
createElement
(
'tr'
));
tr
:=
TJSHTMLElement
(
document
.
createElement
(
'tr'
));
...
@@ -147,6 +149,31 @@ begin
...
@@ -147,6 +149,31 @@ begin
tdPhone
.
innerText
:=
displayPhone
;
tdPhone
.
innerText
:=
displayPhone
;
tr
.
appendChild
(
tdPhone
);
tr
.
appendChild
(
tdPhone
);
// Username — editable for active devices
tdUser
:=
TJSHTMLElement
(
document
.
createElement
(
'td'
));
if
isActive
then
begin
tdUser
.
innerHTML
:=
'<div class="d-flex gap-1 align-items-center">'
+
'<input type="text" class="form-control form-control-sm devmgr-username-input" '
+
'style="max-width:110px;" value="'
+
AUsername
+
'" '
+
'placeholder="username">'
+
'<button class="btn btn-outline-secondary btn-sm devmgr-username-save">Save</button>'
+
'</div>'
;
var
inp
:=
TJSHTMLElement
(
tdUser
.
querySelector
(
'.devmgr-username-input'
));
var
saveBtn
:=
TJSHTMLElement
(
tdUser
.
querySelector
(
'.devmgr-username-save'
));
if
Assigned
(
saveBtn
)
and
Assigned
(
inp
)
then
saveBtn
.
addEventListener
(
'click'
,
procedure
(
Event
:
TJSMouseEvent
)
begin
UpdateDeviceUsername
(
ACredentialId
,
string
(
TJSHTMLInputElement
(
inp
).
value
));
end
);
end
else
if
AUsername
<>
''
then
tdUser
.
innerText
:=
AUsername
else
tdUser
.
innerHTML
:=
'<em class="text-muted small">—</em>'
;
tr
.
appendChild
(
tdUser
);
// User agent (truncated)
// User agent (truncated)
tdAgent
:=
TJSHTMLElement
(
document
.
createElement
(
'td'
));
tdAgent
:=
TJSHTMLElement
(
document
.
createElement
(
'td'
));
if
not
isPending
then
if
not
isPending
then
...
@@ -263,6 +290,7 @@ begin
...
@@ -263,6 +290,7 @@ begin
string
(
item
[
'credential_id'
]),
string
(
item
[
'credential_id'
]),
string
(
item
[
'device_name'
]),
string
(
item
[
'device_name'
]),
string
(
item
[
'phone_number'
]),
string
(
item
[
'phone_number'
]),
string
(
item
[
'username'
]),
string
(
item
[
'user_agent'
]),
string
(
item
[
'user_agent'
]),
string
(
item
[
'registered_at'
]),
string
(
item
[
'registered_at'
]),
string
(
item
[
'status'
])
string
(
item
[
'status'
])
...
@@ -349,6 +377,27 @@ begin
...
@@ -349,6 +377,27 @@ begin
AddPendingDevice
;
AddPendingDevice
;
end
;
end
;
procedure
TFViewDeviceManager
.
UpdateDeviceUsername
(
const
ACredentialId
,
AUsername
:
string
);
var
resp
:
TXDataClientResponse
;
res
:
TJSObject
;
status
:
string
;
begin
try
resp
:=
await
(
XDataWebClient
.
RawInvokeAsync
(
'IApiService.UpdateDeviceUsername'
,
[
ACredentialId
,
AUsername
]));
res
:=
TJSObject
(
resp
.
Result
);
status
:=
string
(
res
[
'status'
]);
if
status
=
'ok'
then
ShowNotification
(
'Username updated.'
,
False
)
else
ShowNotification
(
string
(
res
[
'message'
]));
except
on
E
:
Exception
do
ShowNotification
(
'Update failed: '
+
E
.
Message
);
end
;
end
;
procedure
TFViewDeviceManager
.
AddPendingDevice
;
procedure
TFViewDeviceManager
.
AddPendingDevice
;
var
var
deviceName
,
phoneNumber
:
string
;
deviceName
,
phoneNumber
:
string
;
...
...
webEMIMobile/View.Login.html
View file @
886d9a67
...
@@ -29,28 +29,46 @@
...
@@ -29,28 +29,46 @@
aria-label=
"Close"
></button>
aria-label=
"Close"
></button>
</div>
</div>
<div
class=
"mb-3"
>
<!-- Auto-login section (shown when device has a stored username) -->
<input
id=
"view.login.edtusername"
<div
id=
"view.login.autosection"
class=
"d-none"
>
class=
"form-control"
<p
class=
"text-center text-muted mb-1 small"
>
Signing in as
</p>
type=
"text"
<p
id=
"view.login.autouserlabel"
placeholder=
"Username"
class=
"text-center fw-semibold fs-5 mb-3"
></p>
autofocus
>
<button
id=
"view.login.btnpasskeylogin"
class=
"btn btn-primary w-100 mb-2"
>
Sign In with Passkey
</button>
<div
class=
"text-center"
>
<a
id=
"view.login.switchmanual"
href=
"#"
class=
"small text-muted"
>
Use a different account
</a>
</div>
</div>
</div>
<div
class=
"mb-3"
>
<input
id=
"view.login.edtpassword"
<!-- Manual login section -->
class=
"form-control"
<div
id=
"view.login.manualsection"
>
type=
"password"
<div
class=
"mb-3"
>
placeholder=
"Password"
>
<input
id=
"view.login.edtusername"
</div>
class=
"form-control"
<div
class=
"mb-3"
>
type=
"text"
<select
id=
"view.login.edtagency"
class=
"form-select"
>
placeholder=
"Username"
<!-- populated dynamically -->
autofocus
>
</select>
</div>
<div
class=
"mb-3"
>
<input
id=
"view.login.edtpassword"
class=
"form-control"
type=
"password"
placeholder=
"Password"
>
</div>
<div
class=
"mb-3"
>
<select
id=
"view.login.edtagency"
class=
"form-select"
>
<!-- populated dynamically -->
</select>
</div>
<button
id=
"view.login.btnlogin"
class=
"btn btn-primary w-100"
>
Login
</button>
</div>
</div>
<button
id=
"view.login.btnlogin"
class=
"btn btn-primary w-100"
>
Login
</button>
</div>
</div>
<div
class=
"card-footer text-muted small d-flex justify-content-between"
>
<div
class=
"card-footer text-muted small d-flex justify-content-between"
>
<span>
Please use your lems username
&
password to login.
</span>
<span>
Please use your lems username
&
password to login.
</span>
...
...
webEMIMobile/View.Login.pas
View file @
886d9a67
...
@@ -25,12 +25,15 @@ type
...
@@ -25,12 +25,15 @@ type
private
private
FLoginProc
:
TSuccessProc
;
FLoginProc
:
TSuccessProc
;
FMessage
:
string
;
FMessage
:
string
;
FAutoUsername
:
string
;
FAutoAgency
:
string
;
procedure
ShowNotification
(
Notification
:
string
);
procedure
ShowNotification
(
Notification
:
string
);
procedure
HideNotification
;
procedure
HideNotification
;
procedure
GetAgencyConfigList
;
procedure
GetAgencyConfigList
;
procedure
SetBusy
(
ABusy
:
Boolean
);
procedure
SetBusy
(
ABusy
:
Boolean
);
procedure
DoWebAuthnGet
(
AUser
,
APassword
,
AAgency
,
ACredentialId
,
procedure
DoWebAuthnGet
(
AUser
,
APassword
,
AAgency
,
ACredentialId
,
AChallenge
,
AChallengeToken
:
string
);
AChallenge
,
AChallengeToken
:
string
);
procedure
DoPasskeyAutoLogin
(
ACredentialId
,
AChallenge
,
AChallengeToken
:
string
);
public
public
class
procedure
Display
(
LoginProc
:
TSuccessProc
);
overload
;
class
procedure
Display
(
LoginProc
:
TSuccessProc
);
overload
;
class
procedure
Display
(
LoginProc
:
TSuccessProc
;
AMsg
:
string
);
overload
;
class
procedure
Display
(
LoginProc
:
TSuccessProc
;
AMsg
:
string
);
overload
;
...
@@ -69,16 +72,80 @@ end;
...
@@ -69,16 +72,80 @@ end;
procedure
TFViewLogin
.
WebFormCreate
(
Sender
:
TObject
);
procedure
TFViewLogin
.
WebFormCreate
(
Sender
:
TObject
);
var
var
el
:
TJSElement
;
el
:
TJSElement
;
credId
:
string
;
procedure
OnDeviceUser
(
AUsername
,
AAgency
:
string
);
begin
FAutoUsername
:=
AUsername
;
FAutoAgency
:=
AAgency
;
if
AUsername
<>
''
then
begin
asm
var
autoSection
=
document
.
getElementById
(
'view.login.autosection'
);
var
manualSection
=
document
.
getElementById
(
'view.login.manualsection'
);
var
lbl
=
document
.
getElementById
(
'view.login.autouserlabel'
);
if
(
autoSection
)
autoSection
.
classList
.
remove
(
'd-none'
);
if
(
manualSection
)
manualSection
.
classList
.
add
(
'd-none'
);
if
(
lbl
)
lbl
.
textContent
=
AUsername
;
end
;
// Wire passkey button
asm
var
btn
=
document
.
getElementById
(
'view.login.btnpasskeylogin'
);
if
(
btn
)
{
btn.addEventListener('click', function() {
document.dispatchEvent(new CustomEvent('login-passkey'));
}
);
}
var
lnk
=
document
.
getElementById
(
'view.login.switchmanual'
);
if
(
lnk
)
{
lnk.addEventListener('click', function(e) {
e.preventDefault();
document.getElementById('view.login.autosection').classList.add('d-none');
document.getElementById('view.login.manualsection').classList.remove('d-none');
}
);
}
end
;
document
.
addEventListener
(
'login-passkey'
,
procedure
(
Event
:
TJSEvent
)
var
onBeginOK
:
TOnBeginSuccess
;
onBeginErr
:
TOnLoginError
;
storedCredId
:
string
;
begin
storedCredId
:=
AuthService
.
GetCredentialId
;
SetBusy
(
True
);
HideNotification
;
onBeginOK
:=
procedure
(
AChallenge
,
AChallengeToken
:
string
)
begin
DoPasskeyAutoLogin
(
storedCredId
,
AChallenge
,
AChallengeToken
);
end
;
onBeginErr
:=
procedure
(
AMsg
:
string
)
begin
SetBusy
(
False
);
ShowNotification
(
'Login Error: '
+
AMsg
);
end
;
AuthService
.
BeginAuthentication
(
storedCredId
,
onBeginOK
,
onBeginErr
);
end
);
end
;
end
;
begin
begin
GetAgencyConfigList
;
GetAgencyConfigList
;
el
:=
Document
.
getElementById
(
'view.login.version'
);
el
:=
Document
.
getElementById
(
'view.login.version'
);
if
Assigned
(
el
)
then
if
Assigned
(
el
)
then
TJSHtmlElement
(
el
).
innerText
:=
'v'
+
TDMConnection
.
clientVersion
;
TJSHtmlElement
(
el
).
innerText
:=
'v'
+
TDMConnection
.
clientVersion
;
GetAgencyConfigList
();
if
FMessage
<>
''
then
if
FMessage
<>
''
then
ShowNotification
(
FMessage
)
ShowNotification
(
FMessage
)
else
else
HideNotification
;
HideNotification
;
credId
:=
AuthService
.
GetCredentialId
;
if
credId
<>
''
then
AuthService
.
GetDeviceUser
(
credId
,
@
OnDeviceUser
);
end
;
end
;
procedure
TFViewLogin
.
SetBusy
(
ABusy
:
Boolean
);
procedure
TFViewLogin
.
SetBusy
(
ABusy
:
Boolean
);
...
@@ -208,6 +275,82 @@ begin
...
@@ -208,6 +275,82 @@ begin
end
;
end
;
end
;
end
;
procedure
TFViewLogin
.
DoPasskeyAutoLogin
(
ACredentialId
,
AChallenge
,
AChallengeToken
:
string
);
var
credentialId
,
challenge
,
challengeToken
:
string
;
procedure
OnLoginOK
;
begin
FLoginProc
;
end
;
procedure
OnLoginError
(
AMsg
:
string
);
begin
SetBusy
(
False
);
ShowNotification
(
'Login Error: '
+
AMsg
);
// If auto-login fails, fall back to manual form
asm
var
autoSection
=
document
.
getElementById
(
'view.login.autosection'
);
var
manualSection
=
document
.
getElementById
(
'view.login.manualsection'
);
if
(
autoSection
)
autoSection
.
classList
.
add
(
'd-none'
);
if
(
manualSection
)
manualSection
.
classList
.
remove
(
'd-none'
);
end
;
end
;
procedure
OnAssertion
(
AAuthData
,
AClientDataJSON
,
ASignature
:
string
);
begin
AuthService
.
LoginAutomatic
(
credentialId
,
challengeToken
,
AAuthData
,
AClientDataJSON
,
ASignature
,
@
OnLoginOK
,
@
OnLoginError
);
end
;
procedure
OnWebAuthnError
(
AMsg
:
string
);
begin
SetBusy
(
False
);
ShowNotification
(
'Passkey Error: '
+
AMsg
);
end
;
begin
credentialId
:=
ACredentialId
;
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, '');
}
navigator
.
credentials
.
get
(
{
publicKey: {
challenge: b64urlToArr(challenge),
rpId: window.location.hostname,
allowCredentials: [{ id: b64urlToArr(credentialId), type: 'public-key' }
],
timeout
:
60000
,
userVerification
:
'required'
}
}).
then
(
function
(
assertion
)
{
var authData = arrToB64url(assertion.response.authenticatorData);
var cdJson = arrToB64url(assertion.response.clientDataJSON);
var sig = arrToB64url(assertion.response.signature);
OnAssertion(authData, cdJson, sig);
}
).
catch
(
function
(
err
)
{
OnWebAuthnError('WebAuthn error: ' + err.message);
}
);
})();
end
;
end
;
procedure
TFViewLogin
.
GetAgencyConfigList
;
procedure
TFViewLogin
.
GetAgencyConfigList
;
procedure
OnLoad
(
Response
:
TXDataClientResponse
);
procedure
OnLoad
(
Response
:
TXDataClientResponse
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment