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
7fa82e6d
Commit
7fa82e6d
authored
Oct 22, 2026
by
Mac Stephens
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fix WC device management, SMS verification handling, and map marker loading
parent
13ffaf77
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
230 additions
and
169 deletions
+230
-169
.gitignore
.gitignore
+12
-0
Api.Service.pas
emiMobileServer/Source/Api.Service.pas
+1
-2
Api.ServiceImpl.pas
emiMobileServer/Source/Api.ServiceImpl.pas
+22
-61
Auth.ServiceImpl.pas
emiMobileServer/Source/Auth.ServiceImpl.pas
+24
-32
Ws.DataModel.pas
emiMobileServer/Source/Ws.DataModel.pas
+9
-5
emiMobileServer.ini
emiMobileServer/bin/emiMobileServer.ini
+1
-1
View.DeviceManager.dfm
webEMIMobile/View.DeviceManager.dfm
+9
-0
View.DeviceManager.html
webEMIMobile/View.DeviceManager.html
+16
-11
View.DeviceManager.pas
webEMIMobile/View.DeviceManager.pas
+121
-47
View.Map.pas
webEMIMobile/View.Map.pas
+15
-10
No files found.
.gitignore
View file @
7fa82e6d
...
@@ -28,3 +28,15 @@ emiMobileServer/Source/__recovery/
...
@@ -28,3 +28,15 @@ emiMobileServer/Source/__recovery/
*.dll
*.dll
webEMIMobile/config/__history/
webEMIMobile/config/__history/
emiMobileServer/Clients/BFD/Web/__history/
emiMobileServer/Clients/WC/Web/__history/
emiMobileServer/Shared/Server/__history/
emiMobileServer/Clients/WC/Web/css/__history/
emiMobileServer/Clients/BFD/Web/Win32/Debug/
emiMobileServer/Clients/WC/Web/Win32/Debug/
emiMobileServer/Source/Api.Service.pas
View file @
7fa82e6d
...
@@ -39,8 +39,7 @@ type
...
@@ -39,8 +39,7 @@ type
function
UnrevokeDevice
(
const
CredentialId
:
string
):
TJSONObject
;
function
UnrevokeDevice
(
const
CredentialId
:
string
):
TJSONObject
;
function
AddPendingDevice
(
const
DeviceName
,
PhoneNumber
,
function
AddPendingDevice
(
const
DeviceName
,
PhoneNumber
,
DeviceType
:
string
):
TJSONObject
;
DeviceType
:
string
):
TJSONObject
;
function
DeletePendingDevice
(
const
PhoneNumber
:
string
):
TJSONObject
;
function
DeleteDevice
(
DeviceId
:
Integer
):
TJSONObject
;
function
DeleteDevice
(
CredentialId
:
string
):
TJSONObject
;
function
SendAppLink
(
const
PhoneNumber
:
string
):
TJSONObject
;
function
SendAppLink
(
const
PhoneNumber
:
string
):
TJSONObject
;
function
UpdateDeviceUsername
(
const
CredentialId
,
Username
:
string
):
TJSONObject
;
function
UpdateDeviceUsername
(
const
CredentialId
,
Username
:
string
):
TJSONObject
;
end
;
end
;
...
...
emiMobileServer/Source/Api.ServiceImpl.pas
View file @
7fa82e6d
...
@@ -45,8 +45,7 @@ type
...
@@ -45,8 +45,7 @@ type
function
UnrevokeDevice
(
const
CredentialId
:
string
):
TJSONObject
;
function
UnrevokeDevice
(
const
CredentialId
:
string
):
TJSONObject
;
function
AddPendingDevice
(
const
DeviceName
,
PhoneNumber
,
function
AddPendingDevice
(
const
DeviceName
,
PhoneNumber
,
DeviceType
:
string
):
TJSONObject
;
DeviceType
:
string
):
TJSONObject
;
function
DeletePendingDevice
(
const
PhoneNumber
:
string
):
TJSONObject
;
function
DeleteDevice
(
DeviceId
:
Integer
):
TJSONObject
;
function
DeleteDevice
(
CredentialId
:
string
):
TJSONObject
;
function
SendAppLink
(
const
PhoneNumber
:
string
):
TJSONObject
;
function
SendAppLink
(
const
PhoneNumber
:
string
):
TJSONObject
;
function
UpdateDeviceUsername
(
const
CredentialId
,
Username
:
string
):
TJSONObject
;
function
UpdateDeviceUsername
(
const
CredentialId
,
Username
:
string
):
TJSONObject
;
end
;
end
;
...
@@ -1626,6 +1625,21 @@ begin
...
@@ -1626,6 +1625,21 @@ begin
try
try
q
.
Connection
:=
conn
;
q
.
Connection
:=
conn
;
// Note: Device names are unique regardless of capitalization or surrounding spaces.
q
.
SQL
.
Text
:=
'SELECT id FROM lems.device_registrations '
+
'WHERE UPPER(TRIM(device_name)) = UPPER(:NAME)'
;
q
.
ParamByName
(
'NAME'
).
AsString
:=
Trim
(
DeviceName
);
q
.
Open
;
if
not
q
.
IsEmpty
then
begin
q
.
Close
;
Result
.
AddPair
(
'status'
,
'error'
);
Result
.
AddPair
(
'message'
,
'A device with that name already exists. Enter a different device name.'
);
Exit
;
end
;
q
.
Close
;
// Reject if a pending entry with this phone already exists
// Reject if a pending entry with this phone already exists
q
.
SQL
.
Text
:=
q
.
SQL
.
Text
:=
'SELECT id FROM lems.device_registrations '
+
'SELECT id FROM lems.device_registrations '
+
...
@@ -1661,72 +1675,20 @@ begin
...
@@ -1661,72 +1675,20 @@ begin
end
;
end
;
end
;
end
;
function
TApiService
.
Delete
PendingDevice
(
const
PhoneNumber
:
string
):
TJSONObject
;
function
TApiService
.
Delete
Device
(
DeviceId
:
Integer
):
TJSONObject
;
var
var
conn
:
TUniConnection
;
conn
:
TUniConnection
;
q
:
TUniQuery
;
q
:
TUniQuery
;
normalizedPhone
:
string
;
begin
begin
RequireAdmin
;
RequireAdmin
;
Result
:=
TJSONObject
.
Create
;
Result
:=
TJSONObject
.
Create
;
TXDataOperationContext
.
Current
.
Handler
.
ManagedObjects
.
Add
(
Result
);
TXDataOperationContext
.
Current
.
Handler
.
ManagedObjects
.
Add
(
Result
);
try
if
DeviceId
<=
0
then
normalizedPhone
:=
NormalizePhoneE164Api
(
PhoneNumber
);
except
on
E
:
Exception
do
begin
Result
.
AddPair
(
'status'
,
'error'
);
Result
.
AddPair
(
'message'
,
E
.
Message
);
Exit
;
end
;
end
;
conn
:=
OpenLemsConnection
;
try
q
:=
TUniQuery
.
Create
(
nil
);
try
q
.
Connection
:=
conn
;
q
.
SQL
.
Text
:=
'DELETE FROM lems.device_registrations '
+
'WHERE phone_number = :PHONE AND status = ''pending'''
;
q
.
ParamByName
(
'PHONE'
).
AsString
:=
normalizedPhone
;
q
.
ExecSQL
;
if
q
.
RowsAffected
>
0
then
begin
Logger
.
Log
(
2
,
'TApiService.DeletePendingDevice - deleted phone: '
+
normalizedPhone
);
Result
.
AddPair
(
'status'
,
'ok'
);
Result
.
AddPair
(
'message'
,
'Pending device removed.'
);
end
else
begin
Result
.
AddPair
(
'status'
,
'error'
);
Result
.
AddPair
(
'message'
,
'Pending device not found.'
);
end
;
finally
q
.
Free
;
end
;
finally
conn
.
Free
;
end
;
end
;
function
TApiService
.
DeleteDevice
(
CredentialId
:
string
):
TJSONObject
;
var
conn
:
TUniConnection
;
q
:
TUniQuery
;
begin
RequireAdmin
;
Result
:=
TJSONObject
.
Create
;
TXDataOperationContext
.
Current
.
Handler
.
ManagedObjects
.
Add
(
Result
);
if
Trim
(
CredentialId
)
=
''
then
begin
begin
Result
.
AddPair
(
'status'
,
'error'
);
Result
.
AddPair
(
'status'
,
'error'
);
Result
.
AddPair
(
'message'
,
'
Credential
Id is required.'
);
Result
.
AddPair
(
'message'
,
'
Device
Id is required.'
);
Exit
;
Exit
;
end
;
end
;
...
@@ -1737,14 +1699,13 @@ begin
...
@@ -1737,14 +1699,13 @@ begin
q
.
Connection
:=
conn
;
q
.
Connection
:=
conn
;
q
.
SQL
.
Text
:=
q
.
SQL
.
Text
:=
'DELETE FROM lems.device_registrations '
+
'DELETE FROM lems.device_registrations '
+
'WHERE
credential_id = :C
ID'
;
'WHERE
id = :
ID'
;
q
.
ParamByName
(
'
CID'
).
AsString
:=
Trim
(
CredentialId
)
;
q
.
ParamByName
(
'
ID'
).
AsInteger
:=
DeviceId
;
q
.
ExecSQL
;
q
.
ExecSQL
;
if
q
.
RowsAffected
>
0
then
if
q
.
RowsAffected
>
0
then
begin
begin
Logger
.
Log
(
2
,
'TApiService.DeleteDevice - deleted credential: '
+
Logger
.
Log
(
2
,
'TApiService.DeleteDevice - deleted id: '
+
IntToStr
(
DeviceId
));
Copy
(
CredentialId
,
1
,
20
));
Result
.
AddPair
(
'status'
,
'ok'
);
Result
.
AddPair
(
'status'
,
'ok'
);
Result
.
AddPair
(
'message'
,
'Device deleted.'
);
Result
.
AddPair
(
'message'
,
'Device deleted.'
);
end
end
...
...
emiMobileServer/Source/Auth.ServiceImpl.pas
View file @
7fa82e6d
...
@@ -29,7 +29,7 @@ type
...
@@ -29,7 +29,7 @@ type
function
LoadUserByName
(
const
User
,
Agency
:
string
):
Boolean
;
function
LoadUserByName
(
const
User
,
Agency
:
string
):
Boolean
;
function
Decrypt
(
inStr
,
keyStr
:
AnsiString
):
AnsiString
;
function
Decrypt
(
inStr
,
keyStr
:
AnsiString
):
AnsiString
;
function
GenerateVerificationCode
:
string
;
function
GenerateVerificationCode
:
string
;
procedure
SendVerificationText
(
const
PhoneNumber
,
Code
:
string
)
;
function
SendVerificationText
(
const
PhoneNumber
,
Code
:
string
):
Boolean
;
// Returns True and sets AChallengeB64 if token is valid and of the expected type
// Returns True and sets AChallengeB64 if token is valid and of the expected type
function
VerifyChallengeToken
(
const
ChallengeToken
,
ExpectedType
:
string
;
function
VerifyChallengeToken
(
const
ChallengeToken
,
ExpectedType
:
string
;
out
AChallengeB64
:
string
):
Boolean
;
out
AChallengeB64
:
string
):
Boolean
;
...
@@ -209,7 +209,7 @@ begin
...
@@ -209,7 +209,7 @@ begin
Result
:=
FormatFloat
(
'000000'
,
value
mod
1000000
);
Result
:=
FormatFloat
(
'000000'
,
value
mod
1000000
);
end
;
end
;
procedure
TAuthService
.
SendVerificationText
(
const
PhoneNumber
,
Code
:
string
)
;
function
TAuthService
.
SendVerificationText
(
const
PhoneNumber
,
Code
:
string
):
Boolean
;
var
var
httpClient
:
THTTPClient
;
httpClient
:
THTTPClient
;
bodyStream
:
TStringStream
;
bodyStream
:
TStringStream
;
...
@@ -239,9 +239,10 @@ begin
...
@@ -239,9 +239,10 @@ begin
response
:=
httpClient
.
Post
(
response
:=
httpClient
.
Post
(
'https://api.twilio.com/2010-04-01/Accounts/'
+
'https://api.twilio.com/2010-04-01/Accounts/'
+
ServerConfig
.
twilioAccountSid
+
'/Messages.json'
,
bodyStream
);
ServerConfig
.
twilioAccountSid
+
'/Messages.json'
,
bodyStream
);
if
response
.
StatusCode
>=
300
then
Result
:=
(
response
.
StatusCode
>=
200
)
and
(
response
.
StatusCode
<
300
);
raise
Exception
.
CreateFmt
(
'Twilio error %d: %s'
,
if
not
Result
then
[
response
.
StatusCode
,
response
.
ContentAsString
]);
Logger
.
Log
(
2
,
Format
(
'Twilio rejected verification SMS, HTTP %d: %s'
,
[
response
.
StatusCode
,
response
.
ContentAsString
]));
finally
finally
bodyStream
.
Free
;
bodyStream
.
Free
;
end
;
end
;
...
@@ -308,6 +309,24 @@ begin
...
@@ -308,6 +309,24 @@ begin
end
;
end
;
code
:=
GenerateVerificationCode
;
code
:=
GenerateVerificationCode
;
try
if
not
SendVerificationText
(
normalizedPhone
,
code
)
then
begin
Result
.
AddPair
(
'status'
,
'error'
);
Result
.
AddPair
(
'message'
,
'The SMS provider rejected the verification message. Contact your administrator to check the phone number and SMS configuration.'
);
Exit
;
end
;
except
on
E
:
Exception
do
begin
Logger
.
Log
(
2
,
'--Error sending registration verification code: '
+
E
.
Message
);
Result
.
AddPair
(
'status'
,
'error'
);
Result
.
AddPair
(
'message'
,
'The verification code could not be sent. Please try again.'
);
Exit
;
end
;
end
;
// Note: Persist the code only after the SMS provider accepts the message.
q
:=
TUniQuery
.
Create
(
nil
);
q
:=
TUniQuery
.
Create
(
nil
);
try
try
...
@@ -330,33 +349,6 @@ begin
...
@@ -330,33 +349,6 @@ begin
q
.
Free
;
q
.
Free
;
end
;
end
;
try
SendVerificationText
(
normalizedPhone
,
code
);
except
on
E
:
Exception
do
begin
q
:=
TUniQuery
.
Create
(
nil
);
try
q
.
Connection
:=
authDB
.
ucLemsOCSO
;
q
.
SQL
.
Text
:=
'UPDATE lems.device_registrations '
+
'SET verification_code = NULL, verification_expires_at = NULL '
+
'WHERE phone_number = :PHONE AND status = ''pending'' '
+
'AND verification_code = :CODE'
;
q
.
ParamByName
(
'PHONE'
).
AsString
:=
normalizedPhone
;
q
.
ParamByName
(
'CODE'
).
AsString
:=
code
;
q
.
ExecSQL
;
finally
q
.
Free
;
end
;
Logger
.
Log
(
2
,
'--Error sending registration verification code: '
+
E
.
Message
);
Result
.
AddPair
(
'status'
,
'error'
);
Result
.
AddPair
(
'message'
,
'The verification code could not be sent. Please try again.'
);
Exit
;
end
;
end
;
Logger
.
Log
(
3
,
Format
(
Logger
.
Log
(
3
,
Format
(
'Verification code sent for device id %d and device name: %s'
,
'Verification code sent for device id %d and device name: %s'
,
[
deviceId
,
deviceName
]));
[
deviceId
,
deviceName
]));
...
...
emiMobileServer/Source/Ws.DataModel.pas
View file @
7fa82e6d
...
@@ -79,12 +79,16 @@ end;
...
@@ -79,12 +79,16 @@ end;
destructor
TWsDataModel
.
Destroy
;
destructor
TWsDataModel
.
Destroy
;
begin
begin
FListenerTimer
.
Enabled
:=
False
;
if
Assigned
(
FListenerTimer
)
then
FListenerTimer
.
Enabled
:=
False
;
FListenerTimer
.
Free
;
FListenerTimer
.
Free
;
FThread
.
Terminate
;
if
Assigned
(
FThread
)
then
FStopEvent
.
SetEvent
;
begin
FThread
.
WaitFor
;
FThread
.
Terminate
;
FThread
.
Free
;
FStopEvent
.
SetEvent
;
FThread
.
WaitFor
;
FThread
.
Free
;
end
;
FListenerDb
.
Free
;
FListenerDb
.
Free
;
FStopEvent
.
Free
;
FStopEvent
.
Free
;
inherited
;
inherited
;
...
...
emiMobileServer/bin/emiMobileServer.ini
View file @
7fa82e6d
[Settings]
[Settings]
LogFileNum
=
15
4
LogFileNum
=
15
5
webClientVersion
=
0.9.4.9
webClientVersion
=
0.9.4.9
MemoLogLevel
=
3
MemoLogLevel
=
3
FileLogLevel
=
4
FileLogLevel
=
4
...
...
webEMIMobile/View.DeviceManager.dfm
View file @
7fa82e6d
...
@@ -81,6 +81,15 @@ object FViewDeviceManager: TFViewDeviceManager
...
@@ -81,6 +81,15 @@ object FViewDeviceManager: TFViewDeviceManager
WidthPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
OnClick = btnLogoutClick
OnClick = btnLogoutClick
end
end
object btnRefresh: TWebButton
Left = 620
Top = 50
Width = 75
Height = 25
Caption = 'Refresh'
ElementID = 'view.devmgr.btnrefresh'
OnClick = btnRefreshClick
end
object XDataWebClient: TXDataWebClient
object XDataWebClient: TXDataWebClient
Connection = DMConnection.ApiConnection
Connection = DMConnection.ApiConnection
Left = 800
Left = 800
...
...
webEMIMobile/View.DeviceManager.html
View file @
7fa82e6d
<div
class=
"container-fluid p-3 h-100 d-flex flex-column"
>
<div
class=
"container-fluid p-3 h-100 d-flex flex-column"
>
<div
class=
"d-flex align-items-center mb-3"
>
<div
class=
"d-flex align-items-center
flex-wrap gap-2
mb-3"
>
<h5
class=
"mb-0 me-auto"
>
Device Management
</h5>
<h5
class=
"mb-0 me-auto"
>
Device Management
</h5>
<button
id=
"view.devmgr.btnlogout"
class=
"btn btn-outline-secondary btn-sm me-2"
type=
"button"
>
<button
id=
"view.devmgr.btnlogout"
class=
"btn btn-outline-secondary btn-sm me-2"
type=
"button"
>
Log Out
Log Out
</button>
</button>
<button
id=
"view.devmgr.btnrefresh"
<button
id=
"view.devmgr.btnrefresh"
class=
"btn btn-outline-secondary btn-sm"
class=
"btn btn-outline-secondary btn-sm"
onclick=
"document.dispatchEvent(new CustomEvent('devmgr-refresh'))
"
>
type=
"button
"
>
Refresh
Refresh
</button>
</button>
</div>
</div>
...
@@ -26,9 +26,9 @@
...
@@ -26,9 +26,9 @@
<!-- Add pending device -->
<!-- Add pending device -->
<div
class=
"card mb-3"
>
<div
class=
"card mb-3"
>
<div
class=
"card-body py-2"
>
<div
class=
"card-body py-2"
>
<div
class=
"
d-flex align-items-start gap-2 flex-wrap
"
>
<div
class=
"
row g-2 align-items-start
"
>
<
span
class=
"fw-semibold text-nowrap small mt-2"
>
Add Device:
</span
>
<
div
class=
"col-12 col-md-auto"
><span
class=
"fw-semibold text-nowrap small"
>
Add Device:
</span></div
>
<div
style=
"width:180px;
"
>
<div
class=
"col-6 col-md-auto
"
>
<input
type=
"text"
<input
type=
"text"
id=
"view.devmgr.newname"
id=
"view.devmgr.newname"
class=
"form-control form-control-sm"
class=
"form-control form-control-sm"
...
@@ -36,7 +36,7 @@
...
@@ -36,7 +36,7 @@
required
>
required
>
<div
class=
"invalid-feedback small"
>
Device name is required.
</div>
<div
class=
"invalid-feedback small"
>
Device name is required.
</div>
</div>
</div>
<div
style=
"width:160px;
"
>
<div
class=
"col-6 col-md-auto
"
>
<input
type=
"tel"
<input
type=
"tel"
id=
"view.devmgr.newphone"
id=
"view.devmgr.newphone"
class=
"form-control form-control-sm"
class=
"form-control form-control-sm"
...
@@ -44,7 +44,7 @@
...
@@ -44,7 +44,7 @@
required
>
required
>
<div
class=
"invalid-feedback small"
>
Phone number is required.
</div>
<div
class=
"invalid-feedback small"
>
Phone number is required.
</div>
</div>
</div>
<div
style=
"width:140px;
"
>
<div
class=
"col-6 col-md-auto
"
>
<select
id=
"view.devmgr.newtype"
<select
id=
"view.devmgr.newtype"
class=
"form-select form-select-sm"
class=
"form-select form-select-sm"
required
>
required
>
...
@@ -56,20 +56,24 @@
...
@@ -56,20 +56,24 @@
</select>
</select>
<div
class=
"invalid-feedback small"
>
Device type is required.
</div>
<div
class=
"invalid-feedback small"
>
Device type is required.
</div>
</div>
</div>
<button
id=
"view.devmgr.btnadd"
<div
class=
"col-6 col-md-auto d-grid"
>
class=
"btn btn-primary btn-sm"
>
Add
</button>
<button
id=
"view.devmgr.btnadd"
class=
"btn btn-primary btn-sm"
>
Add
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Device table -->
<!-- Device table -->
<div
class=
"table-responsive flex-grow-1"
>
<div
class=
"flex-grow-1 overflow-auto"
>
<div
id=
"view.devmgr.cards"
class=
"d-md-none"
></div>
<div
class=
"table-responsive d-none d-md-block"
>
<table
class=
"table table-sm table-hover align-middle"
id=
"view.devmgr.table"
>
<table
class=
"table table-sm table-hover align-middle"
id=
"view.devmgr.table"
>
<thead
class=
"table-light sticky-top"
>
<thead
class=
"table-light sticky-top"
>
<tr>
<tr>
<th
style=
"min-width:130px;"
>
Device Name
</th>
<th
style=
"min-width:130px;"
>
Device Name
</th>
<th
style=
"min-width:100px;"
>
Device Type
</th>
<th
style=
"min-width:120px;"
>
Phone
</th>
<th
style=
"min-width:120px;"
>
Phone
</th>
<th
style=
"min-width:1
45
px;"
>
Verification Code
</th>
<th
style=
"min-width:1
10
px;"
>
Verification Code
</th>
<th
style=
"min-width:140px;"
>
Username
</th>
<th
style=
"min-width:140px;"
>
Username
</th>
<th>
Browser / User Agent
</th>
<th>
Browser / User Agent
</th>
<th
style=
"min-width:135px;"
>
Registered
</th>
<th
style=
"min-width:135px;"
>
Registered
</th>
...
@@ -82,6 +86,7 @@
...
@@ -82,6 +86,7 @@
<tbody
id=
"view.devmgr.tbody"
>
<tbody
id=
"view.devmgr.tbody"
>
</tbody>
</tbody>
</table>
</table>
</div>
<p
id=
"view.devmgr.empty"
class=
"text-muted d-none text-center py-4"
>
<p
id=
"view.devmgr.empty"
class=
"text-muted d-none text-center py-4"
>
No devices found.
No devices found.
</p>
</p>
...
...
webEMIMobile/View.DeviceManager.pas
View file @
7fa82e6d
...
@@ -17,6 +17,8 @@ type
...
@@ -17,6 +17,8 @@ type
edtNewPhoneNumber
:
TWebEdit
;
edtNewPhoneNumber
:
TWebEdit
;
btnAddDevice
:
TWebButton
;
btnAddDevice
:
TWebButton
;
btnLogout
:
TWebButton
;
btnLogout
:
TWebButton
;
btnRefresh
:
TWebButton
;
procedure
btnRefreshClick
(
Sender
:
TObject
);
procedure
btnLogoutClick
(
Sender
:
TObject
);
procedure
btnLogoutClick
(
Sender
:
TObject
);
procedure
WebFormCreate
(
Sender
:
TObject
);
procedure
WebFormCreate
(
Sender
:
TObject
);
procedure
btnCloseNotificationClick
(
Sender
:
TObject
);
procedure
btnCloseNotificationClick
(
Sender
:
TObject
);
...
@@ -25,17 +27,15 @@ type
...
@@ -25,17 +27,15 @@ 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
,
ADeviceType
,
procedure
AddDeviceRow
(
ADeviceId
:
Integer
;
const
ACredentialId
,
AName
,
ADeviceType
,
APhoneNumber
,
AUsername
,
AUserAgent
,
ARegisteredAt
,
AStatus
,
APhoneNumber
,
AUsername
,
AUserAgent
,
ARegisteredAt
,
AStatus
,
AVerificationCode
:
string
;
AVerificationCode
:
string
;
AVerificationCodeActive
:
Boolean
);
AVerificationCodeActive
,
AMobile
:
Boolean
);
[
async
]
procedure
LoadDevices
;
[
async
]
procedure
LoadDevices
;
[
async
]
procedure
RevokeDevice
(
const
ACredentialId
,
AName
:
string
);
[
async
]
procedure
RevokeDevice
(
const
ACredentialId
,
AName
:
string
);
[
async
]
procedure
UnrevokeDevice
(
const
ACredentialId
,
AName
:
string
);
[
async
]
procedure
UnrevokeDevice
(
const
ACredentialId
,
AName
:
string
);
procedure
ConfirmDeleteDevice
(
ACredentialId
,
APhoneNumber
,
procedure
ConfirmDeleteDevice
(
ADeviceId
:
Integer
;
ACredentialId
,
AName
:
string
);
AName
:
string
;
AIsPending
:
Boolean
);
[
async
]
procedure
DeleteDevice
(
ADeviceId
:
Integer
;
AName
:
string
;
AIsCurrentDevice
:
Boolean
);
[
async
]
procedure
DeletePendingDevice
(
const
APhoneNumber
,
AName
:
string
);
[
async
]
procedure
DeleteDevice
(
ACredentialId
,
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
);
[
async
]
procedure
UpdateDeviceUsername
(
const
ACredentialId
,
AUsername
:
string
);
...
@@ -49,6 +49,7 @@ implementation
...
@@ -49,6 +49,7 @@ implementation
uses
uses
Auth
.
Service
,
Auth
.
Service
,
Module
.
Websocket
,
Utils
;
Utils
;
{$R *.dfm}
{$R *.dfm}
...
@@ -67,6 +68,12 @@ begin
...
@@ -67,6 +68,12 @@ begin
Result
:=
AE164
;
Result
:=
AE164
;
end
;
end
;
procedure
TFViewDeviceManager
.
btnRefreshClick
(
Sender
:
TObject
);
begin
HideNotification
;
LoadDevices
;
end
;
procedure
TFViewDeviceManager
.
WebFormCreate
(
Sender
:
TObject
);
procedure
TFViewDeviceManager
.
WebFormCreate
(
Sender
:
TObject
);
begin
begin
btnLogout
.
Visible
:=
AuthService
.
DeviceManagementMode
;
btnLogout
.
Visible
:=
AuthService
.
DeviceManagementMode
;
...
@@ -145,20 +152,39 @@ begin
...
@@ -145,20 +152,39 @@ begin
asm
asm
var
tbody
=
document
.
getElementById
(
'view.devmgr.tbody'
);
var
tbody
=
document
.
getElementById
(
'view.devmgr.tbody'
);
if
(
tbody
)
tbody
.
innerHTML
=
''
;
if
(
tbody
)
tbody
.
innerHTML
=
''
;
var
cards
=
document
.
getElementById
(
'view.devmgr.cards'
);
if
(
cards
)
cards
.
innerHTML
=
''
;
var
empty
=
document
.
getElementById
(
'view.devmgr.empty'
);
var
empty
=
document
.
getElementById
(
'view.devmgr.empty'
);
if
(
empty
)
empty
.
classList
.
add
(
'd-none'
);
if
(
empty
)
empty
.
classList
.
add
(
'd-none'
);
end
;
end
;
end
;
end
;
procedure
TFViewDeviceManager
.
AddDeviceRow
(
const
ACredentialId
,
AName
,
procedure
TFViewDeviceManager
.
AddDeviceRow
(
ADeviceId
:
Integer
;
const
ACredentialId
,
AName
,
ADeviceType
,
APhoneNumber
,
AUsername
,
AUserAgent
,
ARegisteredAt
,
AStatus
,
ADeviceType
,
APhoneNumber
,
AUsername
,
AUserAgent
,
ARegisteredAt
,
AStatus
,
AVerificationCode
:
string
;
AVerificationCodeActive
:
Boolean
);
AVerificationCode
:
string
;
AVerificationCodeActive
,
AMobile
:
Boolean
);
var
var
tbody
,
tr
,
tdName
,
tdPhone
,
tdCode
,
tdUser
,
tdAgent
,
tdReg
,
tdStatus
,
tbody
,
tr
,
tdName
,
td
Type
,
td
Phone
,
tdCode
,
tdUser
,
tdAgent
,
tdReg
,
tdStatus
,
tdAccess
,
tdDelete
,
tdSend
:
TJSHTMLElement
;
tdAccess
,
tdDelete
,
tdSend
:
TJSHTMLElement
;
btn
,
btnSend
,
inp
:
TJSHTMLElement
;
btn
,
btnSend
,
inp
:
TJSHTMLElement
;
card
,
cardBody
,
fields
,
details
,
summary
,
actions
:
TJSHTMLElement
;
displayDate
,
displayPhone
:
string
;
displayDate
,
displayPhone
:
string
;
isPending
,
isRevoked
,
isActive
,
isIPhone
,
isAndroid
:
Boolean
;
isPending
,
isRevoked
,
isActive
,
isIPhone
,
isAndroid
:
Boolean
;
procedure
AddCardField
(
Parent
:
TJSHTMLElement
;
LabelText
:
string
;
Cell
:
TJSHTMLElement
);
var
field
,
caption
:
TJSHTMLElement
;
begin
field
:=
TJSHTMLElement
(
document
.
createElement
(
'div'
));
field
.
className
:=
'mb-2 text-break'
;
if
Parent
=
fields
then
field
.
classList
.
add
(
'col-6'
);
caption
:=
TJSHTMLElement
(
document
.
createElement
(
'div'
));
caption
.
className
:=
'small text-muted'
;
caption
.
innerText
:=
LabelText
;
field
.
appendChild
(
caption
);
while
Assigned
(
Cell
.
firstChild
)
do
field
.
appendChild
(
Cell
.
firstChild
);
Parent
.
appendChild
(
field
);
end
;
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
;
...
@@ -182,6 +208,21 @@ begin
...
@@ -182,6 +208,21 @@ begin
tdName
.
innerHTML
:=
'<em class="text-muted">unnamed</em>'
;
tdName
.
innerHTML
:=
'<em class="text-muted">unnamed</em>'
;
tr
.
appendChild
(
tdName
);
tr
.
appendChild
(
tdName
);
tdType
:=
TJSHTMLElement
(
document
.
createElement
(
'td'
));
if
SameText
(
ADeviceType
,
'iphone'
)
then
tdType
.
innerText
:=
'iPhone'
else
if
SameText
(
ADeviceType
,
'android'
)
then
tdType
.
innerText
:=
'Android'
else
if
SameText
(
ADeviceType
,
'browser'
)
then
tdType
.
innerText
:=
'Browser'
else
if
SameText
(
ADeviceType
,
'other'
)
then
tdType
.
innerText
:=
'Other'
else
if
Trim
(
ADeviceType
)
=
''
then
tdType
.
innerText
:=
'—'
else
tdType
.
innerText
:=
ADeviceType
;
tr
.
appendChild
(
tdType
);
// Phone number
// Phone number
tdPhone
:=
TJSHTMLElement
(
document
.
createElement
(
'td'
));
tdPhone
:=
TJSHTMLElement
(
document
.
createElement
(
'td'
));
tdPhone
.
innerText
:=
displayPhone
;
tdPhone
.
innerText
:=
displayPhone
;
...
@@ -299,7 +340,7 @@ begin
...
@@ -299,7 +340,7 @@ begin
btn
.
innerText
:=
'Delete'
;
btn
.
innerText
:=
'Delete'
;
btn
.
addEventListener
(
'click'
,
procedure
(
Event
:
TJSMouseEvent
)
btn
.
addEventListener
(
'click'
,
procedure
(
Event
:
TJSMouseEvent
)
begin
begin
ConfirmDeleteDevice
(
A
CredentialId
,
APhoneNumber
,
AName
,
isPending
);
ConfirmDeleteDevice
(
A
DeviceId
,
ACredentialId
,
AName
);
end
);
end
);
tdDelete
.
appendChild
(
btn
);
tdDelete
.
appendChild
(
btn
);
tr
.
appendChild
(
tdDelete
);
tr
.
appendChild
(
tdDelete
);
...
@@ -324,7 +365,47 @@ begin
...
@@ -324,7 +365,47 @@ begin
else
else
tdSend
.
innerHTML
:=
'<span class="text-muted">—</span>'
;
tdSend
.
innerHTML
:=
'<span class="text-muted">—</span>'
;
tr
.
appendChild
(
tdSend
);
tr
.
appendChild
(
tdSend
);
tbody
.
appendChild
(
tr
);
if
not
AMobile
then
tbody
.
appendChild
(
tr
)
else
begin
card
:=
TJSHTMLElement
(
document
.
createElement
(
'div'
));
card
.
className
:=
'card mb-3'
;
if
isPending
then
card
.
classList
.
add
(
'border-warning'
);
if
isRevoked
then
card
.
classList
.
add
(
'border-secondary'
);
cardBody
:=
TJSHTMLElement
(
document
.
createElement
(
'div'
));
cardBody
.
className
:=
'card-body p-3'
;
card
.
appendChild
(
cardBody
);
fields
:=
TJSHTMLElement
(
document
.
createElement
(
'div'
));
fields
.
className
:=
'row gx-3'
;
cardBody
.
appendChild
(
fields
);
AddCardField
(
fields
,
'Device'
,
tdName
);
AddCardField
(
fields
,
'Type'
,
tdType
);
AddCardField
(
fields
,
'Status'
,
tdStatus
);
AddCardField
(
fields
,
'Phone'
,
tdPhone
);
AddCardField
(
fields
,
'Username'
,
tdUser
);
if
isPending
then
AddCardField
(
fields
,
'Verification code'
,
tdCode
);
details
:=
TJSHTMLElement
(
document
.
createElement
(
'details'
));
details
.
className
:=
'mb-3'
;
summary
:=
TJSHTMLElement
(
document
.
createElement
(
'summary'
));
summary
.
className
:=
'text-primary mb-2'
;
summary
.
innerText
:=
'Details'
;
details
.
appendChild
(
summary
);
AddCardField
(
details
,
'Browser / User Agent'
,
tdAgent
);
AddCardField
(
details
,
'Registered'
,
tdReg
);
cardBody
.
appendChild
(
details
);
actions
:=
TJSHTMLElement
(
document
.
createElement
(
'div'
));
actions
.
className
:=
'd-flex flex-wrap gap-2'
;
if
Assigned
(
tdAccess
.
querySelector
(
'button'
))
then
actions
.
appendChild
(
tdAccess
.
firstChild
);
actions
.
appendChild
(
tdDelete
.
firstChild
);
if
Assigned
(
tdSend
.
querySelector
(
'button'
))
then
actions
.
appendChild
(
tdSend
.
firstChild
);
cardBody
.
appendChild
(
actions
);
TJSHTMLElement
(
document
.
getElementById
(
'view.devmgr.cards'
)).
appendChild
(
card
);
end
;
end
;
end
;
procedure
TFViewDeviceManager
.
LoadDevices
;
procedure
TFViewDeviceManager
.
LoadDevices
;
...
@@ -333,7 +414,7 @@ var
...
@@ -333,7 +414,7 @@ var
list
:
TJSObject
;
list
:
TJSObject
;
data
:
TJSArray
;
data
:
TJSArray
;
item
:
TJSObject
;
item
:
TJSObject
;
i
,
count
:
Integer
;
i
,
count
,
layout
:
Integer
;
begin
begin
ClearTable
;
ClearTable
;
HideNotification
;
HideNotification
;
...
@@ -356,7 +437,9 @@ begin
...
@@ -356,7 +437,9 @@ begin
for
i
:=
0
to
data
.
Length
-
1
do
for
i
:=
0
to
data
.
Length
-
1
do
begin
begin
item
:=
TJSObject
(
data
[
i
]);
item
:=
TJSObject
(
data
[
i
]);
AddDeviceRow
(
for
layout
:=
0
to
1
do
AddDeviceRow
(
Integer
(
item
[
'id'
]),
string
(
item
[
'credential_id'
]),
string
(
item
[
'credential_id'
]),
string
(
item
[
'device_name'
]),
string
(
item
[
'device_name'
]),
string
(
item
[
'device_type'
]),
string
(
item
[
'device_type'
]),
...
@@ -366,7 +449,8 @@ begin
...
@@ -366,7 +449,8 @@ begin
string
(
item
[
'registered_at'
]),
string
(
item
[
'registered_at'
]),
string
(
item
[
'status'
]),
string
(
item
[
'status'
]),
string
(
item
[
'verification_code'
]),
string
(
item
[
'verification_code'
]),
JS
.
toBoolean
(
item
[
'verification_code_active'
])
JS
.
toBoolean
(
item
[
'verification_code_active'
]),
layout
=
1
);
);
end
;
end
;
...
@@ -400,62 +484,52 @@ begin
...
@@ -400,62 +484,52 @@ begin
end
;
end
;
end
;
end
;
procedure
TFViewDeviceManager
.
ConfirmDeleteDevice
(
ACredentialId
,
procedure
TFViewDeviceManager
.
ConfirmDeleteDevice
(
ADeviceId
:
Integer
;
ACredentialId
,
AName
:
string
);
APhoneNumber
,
AName
:
string
;
AIsPending
:
Boolean
);
var
isCurrentDevice
:
Boolean
;
messageText
:
string
;
begin
begin
isCurrentDevice
:=
(
ACredentialId
<>
''
)
and
(
ACredentialId
=
AuthService
.
GetCredentialId
);
messageText
:=
'Are you sure you want to permanently delete device "'
+
AName
+
'"? This cannot be undone.'
;
if
isCurrentDevice
then
messageText
:=
messageText
+
' This is the browser you are using. You will be signed out and must register it again to use emiMobile.'
;
ShowConfirmationModal
(
ShowConfirmationModal
(
'Are you sure you want to permanently delete device "'
+
AName
+
messageText
,
'"? This cannot be undone.'
,
'Cancel'
,
'Delete'
,
'Cancel'
,
'Delete'
,
procedure
(
Confirmed
:
Boolean
)
procedure
(
Confirmed
:
Boolean
)
begin
begin
if
not
Confirmed
then
if
not
Confirmed
then
Exit
;
Exit
;
if
AIsPending
then
DeleteDevice
(
ADeviceId
,
AName
,
isCurrentDevice
);
DeletePendingDevice
(
APhoneNumber
,
AName
)
else
DeleteDevice
(
ACredentialId
,
AName
);
end
);
end
);
end
;
end
;
procedure
TFViewDeviceManager
.
Delete
PendingDevice
(
const
APhoneNumber
,
AName
:
string
);
procedure
TFViewDeviceManager
.
Delete
Device
(
ADeviceId
:
Integer
;
AName
:
string
;
AIsCurrentDevice
:
Boolean
);
var
var
resp
:
TXDataClientResponse
;
resp
:
TXDataClientResponse
;
res
:
TJSObject
;
res
:
TJSObject
;
status
:
string
;
status
:
string
;
begin
begin
try
try
resp
:=
await
(
XDataWebClient
.
RawInvokeAsync
(
'IApiService.Delete
PendingDevice'
,
[
APhoneNumber
]));
resp
:=
await
(
XDataWebClient
.
RawInvokeAsync
(
'IApiService.Delete
Device'
,
[
ADeviceId
]));
res
:=
TJSObject
(
resp
.
Result
);
res
:=
TJSObject
(
resp
.
Result
);
status
:=
string
(
res
[
'status'
]);
status
:=
string
(
res
[
'status'
]);
if
status
=
'ok'
then
if
status
=
'ok'
then
begin
begin
ShowNotification
(
'Pending device "'
+
AName
+
'" removed.'
,
False
);
if
AIsCurrentDevice
then
LoadDevices
;
begin
end
if
Assigned
(
dmWebsocket
)
then
else
dmWebsocket
.
Stop
;
ShowNotification
(
string
(
res
[
'message'
]));
AuthService
.
ClearCredentialId
;
except
AuthService
.
Logout
;
on
E
:
Exception
do
window
.
location
.
reload
(
False
);
ShowNotification
(
'Delete failed: '
+
E
.
Message
);
Exit
;
end
;
end
;
end
;
procedure
TFViewDeviceManager
.
DeleteDevice
(
ACredentialId
,
AName
:
string
);
var
resp
:
TXDataClientResponse
;
res
:
TJSObject
;
status
:
string
;
begin
try
resp
:=
await
(
XDataWebClient
.
RawInvokeAsync
(
'IApiService.DeleteDevice'
,
[
ACredentialId
]));
res
:=
TJSObject
(
resp
.
Result
);
status
:=
string
(
res
[
'status'
]);
if
status
=
'ok'
then
begin
ShowNotification
(
'Device "'
+
AName
+
'" has been deleted.'
,
False
);
ShowNotification
(
'Device "'
+
AName
+
'" has been deleted.'
,
False
);
LoadDevices
;
LoadDevices
;
end
end
...
...
webEMIMobile/View.Map.pas
View file @
7fa82e6d
...
@@ -98,17 +98,22 @@ end;
...
@@ -98,17 +98,22 @@ end;
procedure
TFViewMap
.
UpdateDeviceLocation
(
lat
,
lng
:
Double
);
procedure
TFViewMap
.
UpdateDeviceLocation
(
lat
,
lng
:
Double
);
begin
begin
if
userLocationMarker
=
nil
then
lfMap
.
BeginUpdate
;
begin
try
userLocationMarker
:=
lfMap
.
Markers
.
Add
;
if
userLocationMarker
=
nil
then
userLocationMarker
.
Title
:=
'<div class="small fw-semibold">You are here</div>'
;
begin
userLocationMarker
.
DataString
:=
'device'
;
userLocationMarker
:=
lfMap
.
Markers
.
Add
;
userLocationMarker
.
IconURL
:=
'assets/markers/location_dot.png'
;
userLocationMarker
.
Title
:=
'<div class="small fw-semibold">You are here</div>'
;
userLocationMarker
.
Visible
:=
False
;
userLocationMarker
.
DataString
:=
'device'
;
end
;
userLocationMarker
.
IconURL
:=
'assets/markers/location_dot.png'
;
userLocationMarker
.
Visible
:=
False
;
end
;
userLocationMarker
.
Latitude
:=
lat
;
userLocationMarker
.
Latitude
:=
lat
;
userLocationMarker
.
Longitude
:=
lng
;
userLocationMarker
.
Longitude
:=
lng
;
finally
lfMap
.
EndUpdate
;
end
;
end
;
end
;
...
...
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