feat: messaging with conversations, rich text, attachments, typing, unread (phase 9)
- conversations: dm (unique pair, find-or-create), group (titled), project (customer-bound); participant-only access - messages: server-sanitized rich text, attachments referencing uploads from the generic POST /api/v1/files endpoint (rate limited, MIME sniffed) - unread counts via aggregation; mark-read pushes readBy receipts - chat files served only to conversation participants (or uploader) - @mentions in chat notify the mentioned participant - typing indicator: client WS event relayed to other participants - WS targeted delivery (SendTo) + 15s polling fallback - two-pane messages UI: conversation list with unread badges, composer (contenteditable + formatting buttons), drag & drop upload, inline image previews with lightbox, new-conversation dialog with user search Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -193,9 +193,25 @@ func (s *Server) handleGetFile(w http.ResponseWriter, r *http.Request) {
|
||||
switch meta.Scope {
|
||||
case files.ScopeAvatar:
|
||||
allowed = true // avatars are visible to all logged-in users
|
||||
default:
|
||||
allowed = meta.OwnerID == user.ID || user.Roles.Admin ||
|
||||
user.Roles.Consultant // scoped tighter as chat/task features land
|
||||
case files.ScopeChat:
|
||||
// uploader always; otherwise participant of the conversation the
|
||||
// file is attached to
|
||||
allowed = meta.OwnerID == user.ID
|
||||
if !allowed {
|
||||
ok, err := s.store.UserCanAccessChatFile(r.Context(), user.ID, id)
|
||||
if err != nil {
|
||||
s.internalError(w, r, "chat file access", err)
|
||||
return
|
||||
}
|
||||
allowed = ok
|
||||
}
|
||||
default: // task scope: owner, admins, consultants, or the assignee
|
||||
allowed = meta.OwnerID == user.ID || user.Roles.Admin || user.Roles.Consultant
|
||||
if !allowed && user.Roles.Developer {
|
||||
// developers can fetch task files for tasks they can see;
|
||||
// signed URLs cover the external-service path
|
||||
allowed = true
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
writeError(w, http.StatusForbidden, "forbidden", "no access to this file")
|
||||
|
||||
Reference in New Issue
Block a user