Harden relay configuration and message delivery
This commit is contained in:
parent
2bd9c1b3ae
commit
97628aa9e9
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user