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
214bb264
Commit
214bb264
authored
Sep 01, 2026
by
Michael Brachmann
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
device management progress
parent
19737eac
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
315 additions
and
50 deletions
+315
-50
Api.Service.pas
emiMobileServer/Source/Api.Service.pas
+2
-0
Api.ServiceImpl.pas
emiMobileServer/Source/Api.ServiceImpl.pas
+108
-3
Auth.Service.pas
emiMobileServer/Source/Auth.Service.pas
+1
-0
Auth.ServiceImpl.pas
emiMobileServer/Source/Auth.ServiceImpl.pas
+46
-32
device_registrations_pending.sql
emiMobileServer/device_registrations_pending.sql
+21
-0
Auth.Service.pas
webEMIMobile/Auth.Service.pas
+4
-4
View.DeviceManager.dfm
webEMIMobile/View.DeviceManager.dfm
+22
-0
View.DeviceManager.html
webEMIMobile/View.DeviceManager.html
+16
-0
View.DeviceManager.pas
webEMIMobile/View.DeviceManager.pas
+95
-11
No files found.
emiMobileServer/Source/Api.Service.pas
View file @
214bb264
...
@@ -34,6 +34,8 @@ type
...
@@ -34,6 +34,8 @@ type
// Device management — requires valid JWT; caller must also have user_admin = true
// Device management — requires valid JWT; caller must also have user_admin = true
[
HttpGet
]
function
GetDeviceList
:
TDeviceList
;
[
HttpGet
]
function
GetDeviceList
:
TDeviceList
;
function
RevokeDevice
(
const
CredentialId
:
string
):
TJSONObject
;
function
RevokeDevice
(
const
CredentialId
:
string
):
TJSONObject
;
function
AddPendingDevice
(
const
DeviceName
:
string
):
TJSONObject
;
function
DeletePendingDevice
(
const
DeviceName
:
string
):
TJSONObject
;
end
;
end
;
implementation
implementation
...
...
emiMobileServer/Source/Api.ServiceImpl.pas
View file @
214bb264
...
@@ -36,6 +36,8 @@ type
...
@@ -36,6 +36,8 @@ type
function
GetComplaintMemos
(
const
CfsId
:
string
):
TJSONObject
;
function
GetComplaintMemos
(
const
CfsId
:
string
):
TJSONObject
;
function
GetDeviceList
:
TDeviceList
;
function
GetDeviceList
:
TDeviceList
;
function
RevokeDevice
(
const
CredentialId
:
string
):
TJSONObject
;
function
RevokeDevice
(
const
CredentialId
:
string
):
TJSONObject
;
function
AddPendingDevice
(
const
DeviceName
:
string
):
TJSONObject
;
function
DeletePendingDevice
(
const
DeviceName
:
string
):
TJSONObject
;
end
;
end
;
implementation
implementation
...
@@ -1261,7 +1263,7 @@ begin
...
@@ -1261,7 +1263,7 @@ begin
q
.
Connection
:=
conn
;
q
.
Connection
:=
conn
;
q
.
SQL
.
Text
:=
q
.
SQL
.
Text
:=
'SELECT id, credential_id, device_name, user_agent, '
+
'SELECT id, credential_id, device_name, user_agent, '
+
' registered_at, revoked_at, revoked_by '
+
' 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
;
...
@@ -1281,6 +1283,7 @@ begin
...
@@ -1281,6 +1283,7 @@ begin
else
else
item
.
revoked_at
:=
q
.
FieldByName
(
'revoked_at'
).
AsString
;
item
.
revoked_at
:=
q
.
FieldByName
(
'revoked_at'
).
AsString
;
item
.
revoked_by
:=
q
.
FieldByName
(
'revoked_by'
).
AsString
;
item
.
revoked_by
:=
q
.
FieldByName
(
'revoked_by'
).
AsString
;
item
.
status
:=
q
.
FieldByName
(
'status'
).
AsString
;
Result
.
data
.
Add
(
item
);
Result
.
data
.
Add
(
item
);
q
.
Next
;
q
.
Next
;
...
@@ -1361,8 +1364,8 @@ begin
...
@@ -1361,8 +1364,8 @@ begin
q
.
Connection
:=
conn
;
q
.
Connection
:=
conn
;
q
.
SQL
.
Text
:=
q
.
SQL
.
Text
:=
'UPDATE lems.device_registrations '
+
'UPDATE lems.device_registrations '
+
'SET revoked_at = NOW(), revoked_by = :REVOKED_BY '
+
'SET revoked_at = NOW(), revoked_by = :REVOKED_BY
, status = ''revoked''
'
+
'WHERE credential_id = :CID AND
revoked_at IS NULL
'
;
'WHERE credential_id = :CID AND
status = ''active''
'
;
q
.
ParamByName
(
'REVOKED_BY'
).
AsString
:=
revokedBy
;
q
.
ParamByName
(
'REVOKED_BY'
).
AsString
:=
revokedBy
;
q
.
ParamByName
(
'CID'
).
AsString
:=
Trim
(
CredentialId
);
q
.
ParamByName
(
'CID'
).
AsString
:=
Trim
(
CredentialId
);
q
.
ExecSQL
;
q
.
ExecSQL
;
...
@@ -1388,6 +1391,108 @@ begin
...
@@ -1388,6 +1391,108 @@ begin
end
;
end
;
function
TApiService
.
AddPendingDevice
(
const
DeviceName
:
string
):
TJSONObject
;
var
conn
:
TUniConnection
;
q
:
TUniQuery
;
begin
RequireAdmin
;
Result
:=
TJSONObject
.
Create
;
TXDataOperationContext
.
Current
.
Handler
.
ManagedObjects
.
Add
(
Result
);
if
Trim
(
DeviceName
)
=
''
then
begin
Result
.
AddPair
(
'status'
,
'error'
);
Result
.
AddPair
(
'message'
,
'Device name is required.'
);
Exit
;
end
;
conn
:=
OpenLemsConnection
;
try
q
:=
TUniQuery
.
Create
(
nil
);
try
q
.
Connection
:=
conn
;
// Reject if a pending entry with this name already exists
q
.
SQL
.
Text
:=
'SELECT id FROM lems.device_registrations '
+
'WHERE LOWER(device_name) = LOWER(:NAME) AND status = ''pending'''
;
q
.
ParamByName
(
'NAME'
).
AsString
:=
Trim
(
DeviceName
);
q
.
Open
;
if
not
q
.
IsEmpty
then
begin
q
.
Close
;
Result
.
AddPair
(
'status'
,
'error'
);
Result
.
AddPair
(
'message'
,
'A pending entry for that device name already exists.'
);
Exit
;
end
;
q
.
Close
;
q
.
SQL
.
Text
:=
'INSERT INTO lems.device_registrations (device_name, status) '
+
'VALUES (:NAME, ''pending'')'
;
q
.
ParamByName
(
'NAME'
).
AsString
:=
Trim
(
DeviceName
);
q
.
ExecSQL
;
Logger
.
Log
(
2
,
'TApiService.AddPendingDevice - added "'
+
Trim
(
DeviceName
)
+
'"'
);
Result
.
AddPair
(
'status'
,
'ok'
);
Result
.
AddPair
(
'message'
,
'Pending device added.'
);
finally
q
.
Free
;
end
;
finally
conn
.
Free
;
end
;
end
;
function
TApiService
.
DeletePendingDevice
(
const
DeviceName
:
string
):
TJSONObject
;
var
conn
:
TUniConnection
;
q
:
TUniQuery
;
begin
RequireAdmin
;
Result
:=
TJSONObject
.
Create
;
TXDataOperationContext
.
Current
.
Handler
.
ManagedObjects
.
Add
(
Result
);
if
Trim
(
DeviceName
)
=
''
then
begin
Result
.
AddPair
(
'status'
,
'error'
);
Result
.
AddPair
(
'message'
,
'Device name is required.'
);
Exit
;
end
;
conn
:=
OpenLemsConnection
;
try
q
:=
TUniQuery
.
Create
(
nil
);
try
q
.
Connection
:=
conn
;
q
.
SQL
.
Text
:=
'DELETE FROM lems.device_registrations '
+
'WHERE LOWER(device_name) = LOWER(:NAME) AND status = ''pending'''
;
q
.
ParamByName
(
'NAME'
).
AsString
:=
Trim
(
DeviceName
);
q
.
ExecSQL
;
if
q
.
RowsAffected
>
0
then
begin
Logger
.
Log
(
2
,
'TApiService.DeletePendingDevice - deleted "'
+
Trim
(
DeviceName
)
+
'"'
);
Result
.
AddPair
(
'status'
,
'ok'
);
Result
.
AddPair
(
'message'
,
'Pending device removed.'
);
end
else
begin
Result
.
AddPair
(
'status'
,
'error'
);
Result
.
AddPair
(
'message'
,
'Pending device not found.'
);
end
;
finally
q
.
Free
;
end
;
finally
conn
.
Free
;
end
;
end
;
initialization
initialization
RegisterServiceType
(
TApiService
);
RegisterServiceType
(
TApiService
);
...
...
emiMobileServer/Source/Auth.Service.pas
View file @
214bb264
...
@@ -49,6 +49,7 @@ type
...
@@ -49,6 +49,7 @@ type
registered_at
:
string
;
registered_at
:
string
;
revoked_at
:
string
;
revoked_at
:
string
;
revoked_by
:
string
;
revoked_by
:
string
;
status
:
string
;
end
;
end
;
TDeviceList
=
class
TDeviceList
=
class
...
...
emiMobileServer/Source/Auth.ServiceImpl.pas
View file @
214bb264
...
@@ -162,12 +162,42 @@ end;
...
@@ -162,12 +162,42 @@ end;
function
TAuthService
.
BeginRegistration
(
const
DeviceName
:
string
):
TJSONObject
;
function
TAuthService
.
BeginRegistration
(
const
DeviceName
:
string
):
TJSONObject
;
var
var
token
,
challengeB64
:
string
;
token
,
challengeB64
:
string
;
q
:
TUniQuery
;
begin
begin
Logger
.
Log
(
2
,
'AuthService.BeginRegistration - deviceName: "'
+
DeviceName
+
'"'
);
Logger
.
Log
(
2
,
'AuthService.BeginRegistration - deviceName: "'
+
DeviceName
+
'"'
);
Result
:=
TJSONObject
.
Create
;
Result
:=
TJSONObject
.
Create
;
TXDataOperationContext
.
Current
.
Handler
.
ManagedObjects
.
Add
(
Result
);
TXDataOperationContext
.
Current
.
Handler
.
ManagedObjects
.
Add
(
Result
);
if
Trim
(
DeviceName
)
=
''
then
begin
Result
.
AddPair
(
'status'
,
'error'
);
Result
.
AddPair
(
'message'
,
'Device name is required.'
);
Exit
;
end
;
// Verify device name was pre-authorized by admin
q
:=
TUniQuery
.
Create
(
nil
);
try
q
.
Connection
:=
authDB
.
ucLemsOCSO
;
q
.
SQL
.
Text
:=
'SELECT id FROM lems.device_registrations '
+
'WHERE LOWER(device_name) = LOWER(:NAME) AND status = ''pending'''
;
q
.
ParamByName
(
'NAME'
).
AsString
:=
Trim
(
DeviceName
);
q
.
Open
;
if
q
.
IsEmpty
then
begin
q
.
Close
;
Logger
.
Log
(
2
,
'BeginRegistration - device name not pre-authorized: "'
+
DeviceName
+
'"'
);
Result
.
AddPair
(
'status'
,
'error'
);
Result
.
AddPair
(
'message'
,
'Device name not recognized. Contact your administrator.'
);
Exit
;
end
;
q
.
Close
;
finally
q
.
Free
;
end
;
token
:=
MakeChallengeToken
(
'reg'
);
token
:=
MakeChallengeToken
(
'reg'
);
challengeB64
:=
token
.
Split
([
':'
],
4
)[
0
];
challengeB64
:=
token
.
Split
([
':'
],
4
)[
0
];
...
@@ -321,60 +351,44 @@ begin
...
@@ -321,60 +351,44 @@ begin
Exit
;
Exit
;
end
;
end
;
// 9.
Store credential
// 9.
Activate the pending row — UPDATE instead of INSERT
q
:=
TUniQuery
.
Create
(
nil
);
q
:=
TUniQuery
.
Create
(
nil
);
try
try
q
.
Connection
:=
authDB
.
ucLemsOCSO
;
q
.
Connection
:=
authDB
.
ucLemsOCSO
;
// Check if already registered (re-registration attempt)
q
.
SQL
.
Text
:=
'SELECT revoked_at FROM lems.device_registrations WHERE credential_id = :CID'
;
q
.
ParamByName
(
'CID'
).
AsString
:=
Trim
(
CredentialId
);
q
.
Open
;
if
not
q
.
IsEmpty
then
begin
if
not
q
.
FieldByName
(
'revoked_at'
).
IsNull
then
begin
q
.
Close
;
Logger
.
Log
(
2
,
'CompleteRegistration - revoked credential: '
+
Copy
(
CredentialId
,
1
,
20
));
Result
.
AddPair
(
'status'
,
'revoked'
);
Result
.
AddPair
(
'message'
,
'Device access has been revoked by an administrator.'
);
Exit
;
end
;
// Already active — treat as success (idempotent)
q
.
Close
;
Logger
.
Log
(
3
,
'CompleteRegistration - already registered'
);
Result
.
AddPair
(
'status'
,
'ok'
);
Result
.
AddPair
(
'message'
,
'Device already registered.'
);
Result
.
AddPair
(
'credentialId'
,
Trim
(
CredentialId
));
Exit
;
end
;
q
.
Close
;
var
ctx
:=
THttpServerContext
.
Current
;
var
ctx
:=
THttpServerContext
.
Current
;
var
userAgent
:
string
:=
''
;
var
userAgent
:
string
:=
''
;
if
ctx
<>
nil
then
if
ctx
<>
nil
then
userAgent
:=
ctx
.
Request
.
Headers
.
Get
(
'User-Agent'
);
userAgent
:=
ctx
.
Request
.
Headers
.
Get
(
'User-Agent'
);
q
.
SQL
.
Text
:=
q
.
SQL
.
Text
:=
'INSERT INTO lems.device_registrations '
+
'UPDATE lems.device_registrations '
+
' (credential_id, device_name, user_agent, public_key_x, public_key_y, public_key_alg, sign_count) '
+
'SET credential_id = :CID, user_agent = :AGENT, '
+
'VALUES (:CID, :NAME, :AGENT, :KEYX, :KEYY, :ALG, :CNT)'
;
' public_key_x = :KEYX, public_key_y = :KEYY, '
+
' public_key_alg = :ALG, sign_count = :CNT, '
+
' status = ''active'', registered_at = NOW() '
+
'WHERE LOWER(device_name) = LOWER(:NAME) AND status = ''pending'''
;
q
.
ParamByName
(
'CID'
).
AsString
:=
Trim
(
CredentialId
);
q
.
ParamByName
(
'CID'
).
AsString
:=
Trim
(
CredentialId
);
q
.
ParamByName
(
'NAME'
).
AsString
:=
Trim
(
DeviceName
);
q
.
ParamByName
(
'AGENT'
).
AsString
:=
userAgent
;
q
.
ParamByName
(
'AGENT'
).
AsString
:=
userAgent
;
q
.
ParamByName
(
'KEYX'
).
AsBytes
:=
pubKeyX
;
q
.
ParamByName
(
'KEYX'
).
AsBytes
:=
pubKeyX
;
q
.
ParamByName
(
'KEYY'
).
AsBytes
:=
pubKeyY
;
q
.
ParamByName
(
'KEYY'
).
AsBytes
:=
pubKeyY
;
q
.
ParamByName
(
'ALG'
).
AsInteger
:=
pubKeyAlg
;
q
.
ParamByName
(
'ALG'
).
AsInteger
:=
pubKeyAlg
;
q
.
ParamByName
(
'CNT'
).
AsInteger
:=
Integer
(
signCount
);
q
.
ParamByName
(
'CNT'
).
AsInteger
:=
Integer
(
signCount
);
q
.
ParamByName
(
'NAME'
).
AsString
:=
Trim
(
DeviceName
);
q
.
ExecSQL
;
q
.
ExecSQL
;
if
q
.
RowsAffected
=
0
then
begin
Logger
.
Log
(
2
,
'CompleteRegistration - no pending row for "'
+
DeviceName
+
'"'
);
Result
.
AddPair
(
'status'
,
'error'
);
Result
.
AddPair
(
'message'
,
'Device name is not pending registration. Contact your administrator.'
);
Exit
;
end
;
finally
finally
q
.
Free
;
q
.
Free
;
end
;
end
;
Logger
.
Log
(
2
,
'CompleteRegistration -
stor
ed credential for "'
+
DeviceName
+
'"'
);
Logger
.
Log
(
2
,
'CompleteRegistration -
activat
ed credential for "'
+
DeviceName
+
'"'
);
Result
.
AddPair
(
'status'
,
'ok'
);
Result
.
AddPair
(
'status'
,
'ok'
);
Result
.
AddPair
(
'message'
,
'Device registered successfully.'
);
Result
.
AddPair
(
'message'
,
'Device registered successfully.'
);
Result
.
AddPair
(
'credentialId'
,
Trim
(
CredentialId
));
Result
.
AddPair
(
'credentialId'
,
Trim
(
CredentialId
));
...
...
emiMobileServer/device_registrations_pending.sql
0 → 100644
View file @
214bb264
-- Migration: add pending-device support to device_registrations
-- Run once against the lems database.
-- 1. Add status column (active for all existing rows)
ALTER
TABLE
lems
.
device_registrations
ADD
COLUMN
IF
NOT
EXISTS
status
VARCHAR
(
10
)
NOT
NULL
DEFAULT
'active'
;
-- 2. Mark already-revoked rows correctly
UPDATE
lems
.
device_registrations
SET
status
=
'revoked'
WHERE
revoked_at
IS
NOT
NULL
AND
status
=
'active'
;
-- 3. Allow credential_id / public-key columns to be NULL for pending rows
ALTER
TABLE
lems
.
device_registrations
ALTER
COLUMN
credential_id
DROP
NOT
NULL
;
ALTER
TABLE
lems
.
device_registrations
ALTER
COLUMN
public_key_x
DROP
NOT
NULL
;
ALTER
TABLE
lems
.
device_registrations
ALTER
COLUMN
public_key_y
DROP
NOT
NULL
;
webEMIMobile/Auth.Service.pas
View file @
214bb264
...
@@ -155,13 +155,13 @@ procedure TAuthService.BeginRegistration(ADeviceName: string;
...
@@ -155,13 +155,13 @@ procedure TAuthService.BeginRegistration(ADeviceName: string;
procedure
OnLoad
(
Response
:
TXDataClientResponse
);
procedure
OnLoad
(
Response
:
TXDataClientResponse
);
var
var
resp
:
JS
.
TJSObject
;
resp
:
JS
.
TJSObject
;
challenge
,
token
,
errMsg
:
string
;
challenge
,
token
,
status
:
string
;
begin
begin
resp
:=
JS
.
TJSObject
(
Response
.
Result
);
resp
:=
JS
.
TJSObject
(
Response
.
Result
);
errMsg
:=
JS
.
toString
(
resp
.
Properties
[
'error
'
]);
status
:=
JS
.
toString
(
resp
.
Properties
[
'status
'
]);
if
errMsg
<>
'
'
then
if
status
=
'error
'
then
begin
begin
AError
(
errMsg
);
AError
(
JS
.
toString
(
resp
.
Properties
[
'message'
])
);
Exit
;
Exit
;
end
;
end
;
challenge
:=
JS
.
toString
(
resp
.
Properties
[
'challenge'
]);
challenge
:=
JS
.
toString
(
resp
.
Properties
[
'challenge'
]);
...
...
webEMIMobile/View.DeviceManager.dfm
View file @
214bb264
...
@@ -36,6 +36,28 @@ object FViewDeviceManager: TFViewDeviceManager
...
@@ -36,6 +36,28 @@ object FViewDeviceManager: TFViewDeviceManager
OnClick = btnCloseNotificationClick
OnClick = btnCloseNotificationClick
end
end
end
end
object edtNewDeviceName: TWebEdit
Left = 8
Top = 50
Width = 200
Height = 25
ElementID = 'view.devmgr.newname'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
TabOrder = 1
end
object btnAddDevice: TWebButton
Left = 220
Top = 50
Width = 75
Height = 25
Caption = 'Add'
ElementID = 'view.devmgr.btnadd'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
TabOrder = 2
OnClick = btnAddDeviceClick
end
object XDataWebClient: TXDataWebClient
object XDataWebClient: TXDataWebClient
Connection = DMConnection.ApiConnection
Connection = DMConnection.ApiConnection
Left = 800
Left = 800
...
...
webEMIMobile/View.DeviceManager.html
View file @
214bb264
...
@@ -20,6 +20,22 @@
...
@@ -20,6 +20,22 @@
aria-label=
"Close"
></button>
aria-label=
"Close"
></button>
</div>
</div>
<!-- Add pending device -->
<div
class=
"card mb-3"
>
<div
class=
"card-body py-2"
>
<div
class=
"d-flex align-items-center gap-2"
>
<label
class=
"form-label mb-0 me-1 fw-semibold text-nowrap"
>
Add Device:
</label>
<input
type=
"text"
id=
"view.devmgr.newname"
class=
"form-control form-control-sm"
placeholder=
"Device name"
style=
"max-width: 220px;"
>
<button
id=
"view.devmgr.btnadd"
class=
"btn btn-primary btn-sm"
>
Add
</button>
</div>
</div>
</div>
<!-- Device table -->
<!-- Device table -->
<div
class=
"table-responsive flex-grow-1"
>
<div
class=
"table-responsive flex-grow-1"
>
<table
class=
"table table-sm table-hover align-middle"
id=
"view.devmgr.table"
>
<table
class=
"table table-sm table-hover align-middle"
id=
"view.devmgr.table"
>
...
...
webEMIMobile/View.DeviceManager.pas
View file @
214bb264
...
@@ -13,16 +13,21 @@ type
...
@@ -13,16 +13,21 @@ type
pnlMessage
:
TWebPanel
;
pnlMessage
:
TWebPanel
;
lblMessage
:
TWebLabel
;
lblMessage
:
TWebLabel
;
btnCloseNotification
:
TWebButton
;
btnCloseNotification
:
TWebButton
;
edtNewDeviceName
:
TWebEdit
;
btnAddDevice
:
TWebButton
;
procedure
WebFormCreate
(
Sender
:
TObject
);
procedure
WebFormCreate
(
Sender
:
TObject
);
procedure
btnCloseNotificationClick
(
Sender
:
TObject
);
procedure
btnCloseNotificationClick
(
Sender
:
TObject
);
procedure
btnAddDeviceClick
(
Sender
:
TObject
);
private
private
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
,
AUserAgent
,
procedure
AddDeviceRow
(
const
ACredentialId
,
AName
,
AUserAgent
,
ARegisteredAt
,
A
RevokedAt
:
string
);
ARegisteredAt
,
A
Status
:
string
);
[
async
]
procedure
LoadDevices
;
[
async
]
procedure
LoadDevices
;
[
async
]
procedure
RevokeDevice
(
const
ACredentialId
,
AName
:
string
);
[
async
]
procedure
RevokeDevice
(
const
ACredentialId
,
AName
:
string
);
[
async
]
procedure
AddPendingDevice
(
const
AName
:
string
);
[
async
]
procedure
DeletePendingDevice
(
const
AName
:
string
);
public
public
end
;
end
;
...
@@ -44,6 +49,19 @@ begin
...
@@ -44,6 +49,19 @@ begin
HideNotification
;
HideNotification
;
end
;
end
;
procedure
TFViewDeviceManager
.
btnAddDeviceClick
(
Sender
:
TObject
);
var
name
:
string
;
begin
name
:=
Trim
(
edtNewDeviceName
.
Text
);
if
name
=
''
then
begin
ShowNotification
(
'Please enter a device name.'
);
Exit
;
end
;
AddPendingDevice
(
name
);
end
;
procedure
TFViewDeviceManager
.
ShowNotification
(
const
AMsg
:
string
;
AIsError
:
Boolean
);
procedure
TFViewDeviceManager
.
ShowNotification
(
const
AMsg
:
string
;
AIsError
:
Boolean
);
begin
begin
lblMessage
.
Caption
:=
AMsg
;
lblMessage
.
Caption
:=
AMsg
;
...
@@ -76,22 +94,21 @@ begin
...
@@ -76,22 +94,21 @@ begin
end
;
end
;
procedure
TFViewDeviceManager
.
AddDeviceRow
(
const
ACredentialId
,
AName
,
AUserAgent
,
procedure
TFViewDeviceManager
.
AddDeviceRow
(
const
ACredentialId
,
AName
,
AUserAgent
,
ARegisteredAt
,
A
RevokedAt
:
string
);
ARegisteredAt
,
A
Status
:
string
);
var
var
tbody
,
tr
,
tdName
,
tdAgent
,
tdReg
,
tdStatus
,
tdAction
:
TJSHTMLElement
;
tbody
,
tr
,
tdName
,
tdAgent
,
tdReg
,
tdStatus
,
tdAction
:
TJSHTMLElement
;
btn
:
TJSHTMLElement
;
btn
:
TJSHTMLElement
;
isRevoked
:
Boolean
;
displayDate
:
string
;
displayDate
:
string
;
begin
begin
tbody
:=
TJSHTMLElement
(
document
.
getElementById
(
'view.devmgr.tbody'
));
tbody
:=
TJSHTMLElement
(
document
.
getElementById
(
'view.devmgr.tbody'
));
if
not
Assigned
(
tbody
)
then
if
not
Assigned
(
tbody
)
then
Exit
;
Exit
;
isRevoked
:=
ARevokedAt
<>
''
;
tr
:=
TJSHTMLElement
(
document
.
createElement
(
'tr'
));
tr
:=
TJSHTMLElement
(
document
.
createElement
(
'tr'
));
if
isRevoked
then
if
AStatus
=
'revoked'
then
tr
.
classList
.
add
(
'table-secondary'
);
tr
.
classList
.
add
(
'table-secondary'
)
else
if
AStatus
=
'pending'
then
tr
.
classList
.
add
(
'table-warning'
);
// Device name
// Device name
tdName
:=
TJSHTMLElement
(
document
.
createElement
(
'td'
));
tdName
:=
TJSHTMLElement
(
document
.
createElement
(
'td'
));
...
@@ -108,6 +125,9 @@ begin
...
@@ -108,6 +125,9 @@ begin
tdAgent
.
style
.
setProperty
(
'overflow'
,
'hidden'
);
tdAgent
.
style
.
setProperty
(
'overflow'
,
'hidden'
);
tdAgent
.
style
.
setProperty
(
'text-overflow'
,
'ellipsis'
);
tdAgent
.
style
.
setProperty
(
'text-overflow'
,
'ellipsis'
);
tdAgent
.
style
.
setProperty
(
'white-space'
,
'nowrap'
);
tdAgent
.
style
.
setProperty
(
'white-space'
,
'nowrap'
);
if
AStatus
=
'pending'
then
tdAgent
.
innerHTML
:=
'<em class="text-muted">Not yet registered</em>'
else
tdAgent
.
innerText
:=
AUserAgent
;
tdAgent
.
innerText
:=
AUserAgent
;
tr
.
appendChild
(
tdAgent
);
tr
.
appendChild
(
tdAgent
);
...
@@ -116,12 +136,17 @@ begin
...
@@ -116,12 +136,17 @@ begin
if
Length
(
displayDate
)
>=
19
then
if
Length
(
displayDate
)
>=
19
then
displayDate
:=
Copy
(
displayDate
,
1
,
19
).
Replace
(
'T'
,
' '
);
displayDate
:=
Copy
(
displayDate
,
1
,
19
).
Replace
(
'T'
,
' '
);
tdReg
:=
TJSHTMLElement
(
document
.
createElement
(
'td'
));
tdReg
:=
TJSHTMLElement
(
document
.
createElement
(
'td'
));
if
AStatus
=
'pending'
then
tdReg
.
innerHTML
:=
'<em class="text-muted">—</em>'
else
tdReg
.
innerText
:=
displayDate
;
tdReg
.
innerText
:=
displayDate
;
tr
.
appendChild
(
tdReg
);
tr
.
appendChild
(
tdReg
);
// Status badge
// Status badge
tdStatus
:=
TJSHTMLElement
(
document
.
createElement
(
'td'
));
tdStatus
:=
TJSHTMLElement
(
document
.
createElement
(
'td'
));
if
isRevoked
then
if
AStatus
=
'pending'
then
tdStatus
.
innerHTML
:=
'<span class="badge bg-warning text-dark">Pending</span>'
else
if
AStatus
=
'revoked'
then
tdStatus
.
innerHTML
:=
'<span class="badge bg-secondary">Revoked</span>'
tdStatus
.
innerHTML
:=
'<span class="badge bg-secondary">Revoked</span>'
else
else
tdStatus
.
innerHTML
:=
'<span class="badge bg-success">Active</span>'
;
tdStatus
.
innerHTML
:=
'<span class="badge bg-success">Active</span>'
;
...
@@ -129,12 +154,22 @@ begin
...
@@ -129,12 +154,22 @@ begin
// Action button
// Action button
tdAction
:=
TJSHTMLElement
(
document
.
createElement
(
'td'
));
tdAction
:=
TJSHTMLElement
(
document
.
createElement
(
'td'
));
if
not
isRevoked
then
if
AStatus
=
'pending'
then
begin
btn
:=
TJSHTMLElement
(
document
.
createElement
(
'button'
));
btn
.
className
:=
'btn btn-outline-danger btn-sm'
;
btn
.
innerText
:=
'Cancel'
;
btn
.
addEventListener
(
'click'
,
procedure
(
Event
:
TJSMouseEvent
)
begin
DeletePendingDevice
(
AName
);
end
);
tdAction
.
appendChild
(
btn
);
end
else
if
AStatus
=
'active'
then
begin
begin
btn
:=
TJSHTMLElement
(
document
.
createElement
(
'button'
));
btn
:=
TJSHTMLElement
(
document
.
createElement
(
'button'
));
btn
.
className
:=
'btn btn-danger btn-sm'
;
btn
.
className
:=
'btn btn-danger btn-sm'
;
btn
.
innerText
:=
'Revoke'
;
btn
.
innerText
:=
'Revoke'
;
// Capture token and name in a Delphi closure
btn
.
addEventListener
(
'click'
,
procedure
(
Event
:
TJSMouseEvent
)
btn
.
addEventListener
(
'click'
,
procedure
(
Event
:
TJSMouseEvent
)
begin
begin
RevokeDevice
(
ACredentialId
,
AName
);
RevokeDevice
(
ACredentialId
,
AName
);
...
@@ -182,7 +217,7 @@ begin
...
@@ -182,7 +217,7 @@ begin
string
(
item
[
'device_name'
]),
string
(
item
[
'device_name'
]),
string
(
item
[
'user_agent'
]),
string
(
item
[
'user_agent'
]),
string
(
item
[
'registered_at'
]),
string
(
item
[
'registered_at'
]),
string
(
item
[
'
revoked_at
'
])
string
(
item
[
'
status
'
])
);
);
end
;
end
;
...
@@ -216,4 +251,53 @@ begin
...
@@ -216,4 +251,53 @@ begin
end
;
end
;
end
;
end
;
procedure
TFViewDeviceManager
.
AddPendingDevice
(
const
AName
:
string
);
var
resp
:
TXDataClientResponse
;
res
:
TJSObject
;
status
:
string
;
begin
try
resp
:=
await
(
XDataWebClient
.
RawInvokeAsync
(
'IApiService.AddPendingDevice'
,
[
AName
]));
res
:=
TJSObject
(
resp
.
Result
);
status
:=
string
(
res
[
'status'
]);
if
status
=
'ok'
then
begin
edtNewDeviceName
.
Text
:=
''
;
ShowNotification
(
'Device "'
+
AName
+
'" added — waiting for user to register.'
,
False
);
LoadDevices
;
end
else
ShowNotification
(
string
(
res
[
'message'
]));
except
on
E
:
Exception
do
ShowNotification
(
'Add failed: '
+
E
.
Message
);
end
;
end
;
procedure
TFViewDeviceManager
.
DeletePendingDevice
(
const
AName
:
string
);
var
resp
:
TXDataClientResponse
;
res
:
TJSObject
;
status
:
string
;
begin
try
resp
:=
await
(
XDataWebClient
.
RawInvokeAsync
(
'IApiService.DeletePendingDevice'
,
[
AName
]));
res
:=
TJSObject
(
resp
.
Result
);
status
:=
string
(
res
[
'status'
]);
if
status
=
'ok'
then
begin
ShowNotification
(
'Pending device "'
+
AName
+
'" has been cancelled.'
,
False
);
LoadDevices
;
end
else
ShowNotification
(
string
(
res
[
'message'
]));
except
on
E
:
Exception
do
ShowNotification
(
'Cancel failed: '
+
E
.
Message
);
end
;
end
;
end
.
end
.
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