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
|
||||
MessageBubble(
|
||||
message: message,
|
||||
isSent: message.fromName == "You"
|
||||
isSent: message.from == KeychainStore.installationID
|
||||
)
|
||||
.id(message.id)
|
||||
}
|
||||
|
||||
@ -313,6 +313,7 @@ struct WSMessage: Codable, Equatable {
|
||||
var token: String?
|
||||
var peerID: 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 attachment: WSAttachment? // file metadata attached to a chat.message
|
||||
|
||||
@ -326,6 +327,7 @@ struct WSMessage: Codable, Equatable {
|
||||
token: String? = nil,
|
||||
peerID: String? = nil,
|
||||
reason: String? = nil,
|
||||
sentAt: String? = nil,
|
||||
messageIDs: [String]? = nil,
|
||||
attachment: WSAttachment? = nil
|
||||
) {
|
||||
@ -338,6 +340,7 @@ struct WSMessage: Codable, Equatable {
|
||||
self.token = token
|
||||
self.peerID = peerID
|
||||
self.reason = reason
|
||||
self.sentAt = sentAt
|
||||
self.messageIDs = messageIDs
|
||||
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 {
|
||||
switch message.type {
|
||||
case "chat.message":
|
||||
let attachment = message.attachment.map {
|
||||
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(
|
||||
id: message.id ?? UUID().uuidString,
|
||||
from: message.from ?? "unknown",
|
||||
fromName: message.fromName ?? "Unknown",
|
||||
body: message.body ?? "",
|
||||
sentAt: Date(),
|
||||
sentAt: sentAt,
|
||||
attachment: attachment,
|
||||
isRead: false
|
||||
)
|
||||
@ -196,6 +210,7 @@ final class AppSession: ObservableObject {
|
||||
type: "chat.message",
|
||||
id: message.id,
|
||||
body: message.body,
|
||||
sentAt: AppSession.iso8601.string(from: message.sentAt),
|
||||
attachment: attachment
|
||||
)
|
||||
|
||||
|
||||
@ -8,7 +8,8 @@ final class WSMessageTests: XCTestCase {
|
||||
id: "message-id",
|
||||
body: "Hello",
|
||||
from: "peer-id",
|
||||
fromName: "Alex"
|
||||
fromName: "Alex",
|
||||
sentAt: "2026-07-26T18:12:00.123Z"
|
||||
)
|
||||
|
||||
let decodedMessage = try JSONDecoder().decode(
|
||||
|
||||
@ -189,6 +189,38 @@
|
||||
- The manual cleanup endpoint returns unavailable until `ADMIN_TOKEN` is configured.
|
||||
- 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
|
||||
|
||||
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
|
||||
UPLOADS_DIR=/data/files
|
||||
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