Harden relay configuration and message delivery

This commit is contained in:
diyaa 2026-07-26 19:40:58 +02:00
parent 2bd9c1b3ae
commit 97628aa9e9
6 changed files with 57 additions and 5 deletions

View File

@ -23,7 +23,7 @@ struct ChatView: View {
ForEach(session.messages) { message in ForEach(session.messages) { message in
MessageBubble( MessageBubble(
message: message, message: message,
isSent: message.fromName == "You" isSent: message.from == KeychainStore.installationID
) )
.id(message.id) .id(message.id)
} }

View File

@ -313,6 +313,7 @@ struct WSMessage: Codable, Equatable {
var token: String? var token: String?
var peerID: String? var peerID: String?
var reason: String? var reason: String?
var sentAt: String? // ISO-8601 timestamp set by the sender; preserved through the relay
var messageIDs: [String]? // used by read receipts: { type: "read", messageIDs: [...] } var messageIDs: [String]? // used by read receipts: { type: "read", messageIDs: [...] }
var attachment: WSAttachment? // file metadata attached to a chat.message var attachment: WSAttachment? // file metadata attached to a chat.message
@ -326,6 +327,7 @@ struct WSMessage: Codable, Equatable {
token: String? = nil, token: String? = nil,
peerID: String? = nil, peerID: String? = nil,
reason: String? = nil, reason: String? = nil,
sentAt: String? = nil,
messageIDs: [String]? = nil, messageIDs: [String]? = nil,
attachment: WSAttachment? = nil attachment: WSAttachment? = nil
) { ) {
@ -338,6 +340,7 @@ struct WSMessage: Codable, Equatable {
self.token = token self.token = token
self.peerID = peerID self.peerID = peerID
self.reason = reason self.reason = reason
self.sentAt = sentAt
self.messageIDs = messageIDs self.messageIDs = messageIDs
self.attachment = attachment self.attachment = attachment
} }

View File

@ -159,18 +159,32 @@ final class AppSession: ObservableObject {
} }
} }
private static let iso8601: ISO8601DateFormatter = {
let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return f
}()
private func handleIncomingMessage(_ message: WSMessage) async { private func handleIncomingMessage(_ message: WSMessage) async {
switch message.type { switch message.type {
case "chat.message": case "chat.message":
let attachment = message.attachment.map { let attachment = message.attachment.map {
AttachmentInfo(fileID: $0.fileID, name: $0.name, size: $0.size) AttachmentInfo(fileID: $0.fileID, name: $0.name, size: $0.size)
} }
// Use the timestamp the sender embedded; fall back to now only if missing.
let sentAt: Date
if let raw = message.sentAt,
let parsed = AppSession.iso8601.date(from: raw) {
sentAt = parsed
} else {
sentAt = Date()
}
let chatMessage = ChatMessage( let chatMessage = ChatMessage(
id: message.id ?? UUID().uuidString, id: message.id ?? UUID().uuidString,
from: message.from ?? "unknown", from: message.from ?? "unknown",
fromName: message.fromName ?? "Unknown", fromName: message.fromName ?? "Unknown",
body: message.body ?? "", body: message.body ?? "",
sentAt: Date(), sentAt: sentAt,
attachment: attachment, attachment: attachment,
isRead: false isRead: false
) )
@ -196,6 +210,7 @@ final class AppSession: ObservableObject {
type: "chat.message", type: "chat.message",
id: message.id, id: message.id,
body: message.body, body: message.body,
sentAt: AppSession.iso8601.string(from: message.sentAt),
attachment: attachment attachment: attachment
) )

View File

@ -8,7 +8,8 @@ final class WSMessageTests: XCTestCase {
id: "message-id", id: "message-id",
body: "Hello", body: "Hello",
from: "peer-id", from: "peer-id",
fromName: "Alex" fromName: "Alex",
sentAt: "2026-07-26T18:12:00.123Z"
) )
let decodedMessage = try JSONDecoder().decode( let decodedMessage = try JSONDecoder().decode(

View File

@ -189,6 +189,38 @@
- The manual cleanup endpoint returns unavailable until `ADMIN_TOKEN` is configured. - The manual cleanup endpoint returns unavailable until `ADMIN_TOKEN` is configured.
- Requests must use `Authorization: Bearer <ADMIN_TOKEN>`. - Requests must use `Authorization: Bearer <ADMIN_TOKEN>`.
### Deployment configuration hardening
- `relay-server/.env.example` now contains a non-secret placeholder for `ADMIN_TOKEN` instead of a committed credential. Before deployment, generate and set a unique value in the untracked `relay-server/.env` file.
- Verified the Docker Compose configuration with the example environment and confirmed a local relay instance responds successfully from `GET /health`.
- Extended WebSocket serialization coverage to preserve the sender timestamp used for offline message delivery.
## Bug Fixes — Post-Review ✅
**Applied by:** Claude
### Fix 1 — `isSent` detection in ChatView
**File:** `FchatiApp/Sources/FchatiApp/Features/Chat/ChatView.swift`
**Problem:** `MessageBubble` was deciding which side to render on using `message.fromName == "You"`. If the user ever set their display name to "You" this would misidentify received messages as sent.
**Fix:** Compare `message.from == KeychainStore.installationID` — the installation UUID is unique per device and never changes.
---
### Fix 2 — Message timestamps for queued/offline messages
**Files:**
- `FchatiApp/Sources/FchatiApp/Networking/WSClient.swift` — added `sentAt: String?` field to `WSMessage`
- `FchatiApp/Sources/FchatiApp/Session/AppSession.swift` — sender embeds ISO-8601 timestamp; receiver parses it
**Problem:** When a message was delivered from the offline queue, `sentAt` was set to `Date()` (arrival time). Messages sent at 11pm but delivered after a restart showed a wrong timestamp.
**Fix:** The sender now includes `sentAt` as an ISO-8601 string in the WebSocket payload. The relay passes it through unchanged (it's a dumb pipe). The receiver parses it; falls back to `Date()` only if the field is missing (old clients).
---
## Pending ## Pending
See `TASKS.md` for all remaining tasks. Nothing remaining in the codebase. Next step: deploy relay server to production.

View File

@ -2,4 +2,5 @@ PORT=3000
MAX_FILE_SIZE_MB=25 MAX_FILE_SIZE_MB=25
UPLOADS_DIR=/data/files UPLOADS_DIR=/data/files
FILE_TTL_DAYS=30 FILE_TTL_DAYS=30
ADMIN_TOKEN=8545851a901067a271e8e941297acb4c3d8d68c83eddb2809a8184071e8bc46f # Required for POST /admin/cleanup. Generate a unique value, for example: openssl rand -hex 32
ADMIN_TOKEN=replace-with-a-unique-random-secret