Skip to content

Commit 12764c0

Browse files
committed
[devices] public key support
1 parent 4bf5468 commit 12764c0

5 files changed

Lines changed: 61 additions & 14 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/android-sms-gateway/web-dashboard
33
go 1.25.7
44

55
require (
6-
github.com/android-sms-gateway/client-go v1.14.4
6+
github.com/android-sms-gateway/client-go v1.14.5-0.20260812072721-ef4aef654a3f
77
github.com/go-core-fx/cachefx v0.0.3
88
github.com/go-core-fx/config v0.1.0
99
github.com/go-core-fx/fiberfx v0.6.0

internal/server/docs/docs.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -807,11 +807,17 @@ const docTemplate = `{
807807
"isOnline": {
808808
"type": "boolean"
809809
},
810+
"keyVersion": {
811+
"type": "integer"
812+
},
810813
"lastSeen": {
811814
"type": "string"
812815
},
813816
"name": {
814817
"type": "string"
818+
},
819+
"publicKey": {
820+
"type": "string"
815821
}
816822
}
817823
},
@@ -1209,7 +1215,7 @@ const docTemplate = `{
12091215
"phoneNumber": {
12101216
"description": "Phone number or first 16 symbols of SHA256 hash",
12111217
"type": "string",
1212-
"maxLength": 128,
1218+
"maxLength": 512,
12131219
"minLength": 1,
12141220
"example": "79990001234"
12151221
},

internal/server/handlers/devices.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,13 @@ func (h *DevicesHandler) Register(r fiber.Router) {
4242
}
4343

4444
type deviceListItem struct {
45-
ID string `json:"id"`
46-
Name string `json:"name"`
47-
LastSeen time.Time `json:"lastSeen"`
48-
IsOnline bool `json:"isOnline"`
49-
CreatedAt time.Time `json:"createdAt"`
45+
ID string `json:"id"`
46+
Name string `json:"name"`
47+
LastSeen time.Time `json:"lastSeen"`
48+
IsOnline bool `json:"isOnline"`
49+
CreatedAt time.Time `json:"createdAt"`
50+
PublicKey *string `json:"publicKey,omitempty"`
51+
KeyVersion *int `json:"keyVersion,omitempty"`
5052
}
5153

5254
// ListDevices returns registered devices.
@@ -74,11 +76,13 @@ func (h *DevicesHandler) list(c *fiber.Ctx) error {
7476
items := make([]deviceListItem, len(devices))
7577
for i, d := range devices {
7678
items[i] = deviceListItem{
77-
ID: d.ID,
78-
Name: d.Name,
79-
LastSeen: d.LastSeen,
80-
IsOnline: d.DeletedAt == nil && time.Since(d.LastSeen) < dashboard.DeviceOnlineIn,
81-
CreatedAt: d.CreatedAt,
79+
ID: d.ID,
80+
Name: d.Name,
81+
PublicKey: d.PublicKey,
82+
KeyVersion: d.KeyVersion,
83+
LastSeen: d.LastSeen,
84+
IsOnline: d.DeletedAt == nil && time.Since(d.LastSeen) < dashboard.DeviceOnlineIn,
85+
CreatedAt: d.CreatedAt,
8286
}
8387
}
8488

web/src/lib/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ export interface Device {
4444
lastSeen: string;
4545
isOnline: boolean;
4646
createdAt: string;
47+
/** Base64 X.509 SPKI DER of the E2E public key; absent when E2E is not configured. */
48+
publicKey?: string | null;
49+
/** Key rotation version; present only with publicKey. */
50+
keyVersion?: number | null;
4751
}
4852

4953
export interface MessageListItem {

web/src/routes/devices/+page.svelte

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import { onMount } from 'svelte';
3-
import { listDevices, deleteDevice } from '$lib/api';
3+
import { listDevices, deleteDevice, getSettings } from '$lib/api';
44
import { Button } from '$lib/components/ui/button/index.js';
55
import { Badge } from '$lib/components/ui/badge/index.js';
66
import { Skeleton } from '$lib/components/ui/skeleton/index.js';
@@ -12,6 +12,7 @@
1212
let devices = $state<Device[]>([]);
1313
let loading = $state(true);
1414
let error = $state('');
15+
let passphraseEnabled = $state(false);
1516
1617
onMount(load);
1718
@@ -25,6 +26,22 @@
2526
} finally {
2627
loading = false;
2728
}
29+
// Account-wide passphrase encryption is not per-device; used to label
30+
// devices without an E2E key. Failure must not break the listing.
31+
try {
32+
const settings = await getSettings();
33+
passphraseEnabled = Boolean(settings?.encryption?.passphrase);
34+
} catch {
35+
passphraseEnabled = false;
36+
}
37+
}
38+
39+
type EncryptionType = 'e2e' | 'passphrase' | 'none';
40+
41+
function encryptionType(d: Device): EncryptionType {
42+
if (d.publicKey) return 'e2e';
43+
if (passphraseEnabled) return 'passphrase';
44+
return 'none';
2845
}
2946
3047
let deleting = $state<string | null>(null);
@@ -87,6 +104,7 @@
87104
<Th>Name</Th>
88105
<Th>ID</Th>
89106
<Th>Status</Th>
107+
<Th>Encryption</Th>
90108
<Th>Last Seen</Th>
91109
<Th></Th>
92110
</Tr>
@@ -103,7 +121,22 @@
103121
<Badge variant="outline">Offline</Badge>
104122
{/if}
105123
</Td>
106-
<Td class="whitespace-nowrap text-muted-foreground">{timeAgo(d.lastSeen)}</Td>
124+
<Td>
125+
{#if encryptionType(d) === 'e2e'}
126+
<span class="inline-flex items-center gap-1.5" title="End-to-end encryption enabled">
127+
<span class="inline-block h-2 w-2 rounded-full bg-emerald-500" aria-hidden="true"></span>
128+
<Badge variant="secondary">E2E</Badge>
129+
{#if d.keyVersion != null}
130+
<span class="font-mono text-xs text-muted-foreground">v{d.keyVersion}</span>
131+
{/if}
132+
</span>
133+
{:else if encryptionType(d) === 'passphrase'}
134+
<Badge variant="secondary">Passphrase</Badge>
135+
{:else}
136+
<Badge variant="outline">None</Badge>
137+
{/if}
138+
</Td>
139+
<Td class="whitespace-nowrap text-muted-foreground">{timeAgo(d.lastSeen)}</Td>
107140
<Td>
108141
<Button
109142
variant="destructive"

0 commit comments

Comments
 (0)