Commit 91c2a4ce authored by Steven's avatar Steven

refactor: inbox service

parent a4920d46
...@@ -4,6 +4,8 @@ package memos.api.v1; ...@@ -4,6 +4,8 @@ package memos.api.v1;
import "google/api/annotations.proto"; import "google/api/annotations.proto";
import "google/api/client.proto"; import "google/api/client.proto";
import "google/api/field_behavior.proto";
import "google/api/resource.proto";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto"; import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto"; import "google/protobuf/timestamp.proto";
...@@ -13,7 +15,8 @@ option go_package = "gen/api/v1"; ...@@ -13,7 +15,8 @@ option go_package = "gen/api/v1";
service InboxService { service InboxService {
// ListInboxes lists inboxes for a user. // ListInboxes lists inboxes for a user.
rpc ListInboxes(ListInboxesRequest) returns (ListInboxesResponse) { rpc ListInboxes(ListInboxesRequest) returns (ListInboxesResponse) {
option (google.api.http) = {get: "/api/v1/inboxes"}; option (google.api.http) = {get: "/api/v1/{parent=users/*}/inboxes"};
option (google.api.method_signature) = "parent";
} }
// UpdateInbox updates an inbox. // UpdateInbox updates an inbox.
rpc UpdateInbox(UpdateInboxRequest) returns (Inbox) { rpc UpdateInbox(UpdateInboxRequest) returns (Inbox) {
...@@ -31,59 +34,116 @@ service InboxService { ...@@ -31,59 +34,116 @@ service InboxService {
} }
message Inbox { message Inbox {
// The name of the inbox. option (google.api.resource) = {
// Format: inboxes/{id}, id is the system generated auto-incremented id. type: "memos.api.v1/Inbox"
string name = 1; pattern: "inboxes/{inbox}"
name_field: "name"
singular: "inbox"
plural: "inboxes"
};
// The resource name of the inbox.
// Format: inboxes/{inbox}
string name = 1 [(google.api.field_behavior) = IDENTIFIER];
// The sender of the inbox notification.
// Format: users/{user} // Format: users/{user}
string sender = 2; string sender = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
// The receiver of the inbox notification.
// Format: users/{user} // Format: users/{user}
string receiver = 3; string receiver = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
// The status of the inbox notification.
Status status = 4 [(google.api.field_behavior) = OPTIONAL];
// Output only. The creation timestamp.
google.protobuf.Timestamp create_time = 5 [(google.api.field_behavior) = OUTPUT_ONLY];
// The type of the inbox notification.
Type type = 6 [(google.api.field_behavior) = OUTPUT_ONLY];
// Optional. The activity ID associated with this inbox notification.
optional int32 activity_id = 7 [(google.api.field_behavior) = OPTIONAL];
// Status enumeration for inbox notifications.
enum Status { enum Status {
// Unspecified status.
STATUS_UNSPECIFIED = 0; STATUS_UNSPECIFIED = 0;
// The notification is unread.
UNREAD = 1; UNREAD = 1;
// The notification is archived.
ARCHIVED = 2; ARCHIVED = 2;
} }
Status status = 4;
google.protobuf.Timestamp create_time = 5;
// Type enumeration for inbox notifications.
enum Type { enum Type {
// Unspecified type.
TYPE_UNSPECIFIED = 0; TYPE_UNSPECIFIED = 0;
// Memo comment notification.
MEMO_COMMENT = 1; MEMO_COMMENT = 1;
// Version update notification.
VERSION_UPDATE = 2; VERSION_UPDATE = 2;
} }
Type type = 6;
optional int32 activity_id = 7;
} }
message ListInboxesRequest { message ListInboxesRequest {
// Required. The parent resource whose inboxes will be listed.
// Format: users/{user} // Format: users/{user}
string user = 1; string parent = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/User"}
];
// Optional. The maximum number of inboxes to return.
// The service may return fewer than this value.
// If unspecified, at most 50 inboxes will be returned.
// The maximum value is 1000; values above 1000 will be coerced to 1000.
int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL];
// Optional. A page token, received from a previous `ListInboxes` call.
// Provide this to retrieve the subsequent page.
string page_token = 3 [(google.api.field_behavior) = OPTIONAL];
// The maximum number of inbox to return. // Optional. Filter to apply to the list results.
int32 page_size = 2; // Example: "status=UNREAD" or "type=MEMO_COMMENT"
// Supported operators: =, !=
// Supported fields: status, type, sender, create_time
string filter = 4 [(google.api.field_behavior) = OPTIONAL];
// Provide this to retrieve the subsequent page. // Optional. The order to sort results by.
string page_token = 3; // Example: "create_time desc" or "status asc"
string order_by = 5 [(google.api.field_behavior) = OPTIONAL];
} }
message ListInboxesResponse { message ListInboxesResponse {
// The list of inboxes.
repeated Inbox inboxes = 1; repeated Inbox inboxes = 1;
// A token, which can be sent as `page_token` to retrieve the next page. // A token that can be sent as `page_token` to retrieve the next page.
// If this field is omitted, there are no subsequent pages. // If this field is omitted, there are no subsequent pages.
string next_page_token = 2; string next_page_token = 2;
// The total count of inboxes (may be approximate).
int32 total_size = 3;
} }
message UpdateInboxRequest { message UpdateInboxRequest {
Inbox inbox = 1; // Required. The inbox to update.
Inbox inbox = 1 [(google.api.field_behavior) = REQUIRED];
// Required. The list of fields to update.
google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED];
google.protobuf.FieldMask update_mask = 2; // Optional. If set to true, allows updating missing fields.
bool allow_missing = 3 [(google.api.field_behavior) = OPTIONAL];
} }
message DeleteInboxRequest { message DeleteInboxRequest {
// The name of the inbox to delete. // Required. The resource name of the inbox to delete.
string name = 1; // Format: inboxes/{inbox}
string name = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/Inbox"}
];
} }
This diff is collapsed.
...@@ -35,14 +35,23 @@ var ( ...@@ -35,14 +35,23 @@ var (
_ = metadata.Join _ = metadata.Join
) )
var filter_InboxService_ListInboxes_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} var filter_InboxService_ListInboxes_0 = &utilities.DoubleArray{Encoding: map[string]int{"parent": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
func request_InboxService_ListInboxes_0(ctx context.Context, marshaler runtime.Marshaler, client InboxServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { func request_InboxService_ListInboxes_0(ctx context.Context, marshaler runtime.Marshaler, client InboxServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var ( var (
protoReq ListInboxesRequest protoReq ListInboxesRequest
metadata runtime.ServerMetadata metadata runtime.ServerMetadata
err error
) )
io.Copy(io.Discard, req.Body) io.Copy(io.Discard, req.Body)
val, ok := pathParams["parent"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent")
}
protoReq.Parent, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err)
}
if err := req.ParseForm(); err != nil { if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
} }
...@@ -57,7 +66,16 @@ func local_request_InboxService_ListInboxes_0(ctx context.Context, marshaler run ...@@ -57,7 +66,16 @@ func local_request_InboxService_ListInboxes_0(ctx context.Context, marshaler run
var ( var (
protoReq ListInboxesRequest protoReq ListInboxesRequest
metadata runtime.ServerMetadata metadata runtime.ServerMetadata
err error
) )
val, ok := pathParams["parent"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent")
}
protoReq.Parent, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err)
}
if err := req.ParseForm(); err != nil { if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
} }
...@@ -195,7 +213,7 @@ func RegisterInboxServiceHandlerServer(ctx context.Context, mux *runtime.ServeMu ...@@ -195,7 +213,7 @@ func RegisterInboxServiceHandlerServer(ctx context.Context, mux *runtime.ServeMu
var stream runtime.ServerTransportStream var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.InboxService/ListInboxes", runtime.WithHTTPPathPattern("/api/v1/inboxes")) annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.InboxService/ListInboxes", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/inboxes"))
if err != nil { if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return return
...@@ -293,7 +311,7 @@ func RegisterInboxServiceHandlerClient(ctx context.Context, mux *runtime.ServeMu ...@@ -293,7 +311,7 @@ func RegisterInboxServiceHandlerClient(ctx context.Context, mux *runtime.ServeMu
ctx, cancel := context.WithCancel(req.Context()) ctx, cancel := context.WithCancel(req.Context())
defer cancel() defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.InboxService/ListInboxes", runtime.WithHTTPPathPattern("/api/v1/inboxes")) annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.InboxService/ListInboxes", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/inboxes"))
if err != nil { if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return return
...@@ -344,7 +362,7 @@ func RegisterInboxServiceHandlerClient(ctx context.Context, mux *runtime.ServeMu ...@@ -344,7 +362,7 @@ func RegisterInboxServiceHandlerClient(ctx context.Context, mux *runtime.ServeMu
} }
var ( var (
pattern_InboxService_ListInboxes_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "inboxes"}, "")) pattern_InboxService_ListInboxes_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "inboxes"}, ""))
pattern_InboxService_UpdateInbox_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "inboxes", "inbox.name"}, "")) pattern_InboxService_UpdateInbox_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "inboxes", "inbox.name"}, ""))
pattern_InboxService_DeleteInbox_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "inboxes", "name"}, "")) pattern_InboxService_DeleteInbox_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "inboxes", "name"}, ""))
) )
......
...@@ -292,38 +292,6 @@ paths: ...@@ -292,38 +292,6 @@ paths:
type: string type: string
tags: tags:
- IdentityProviderService - IdentityProviderService
/api/v1/inboxes:
get:
summary: ListInboxes lists inboxes for a user.
operationId: InboxService_ListInboxes
responses:
"200":
description: A successful response.
schema:
$ref: '#/definitions/v1ListInboxesResponse'
default:
description: An unexpected error response.
schema:
$ref: '#/definitions/googlerpcStatus'
parameters:
- name: user
description: 'Format: users/{user}'
in: query
required: false
type: string
- name: pageSize
description: The maximum number of inbox to return.
in: query
required: false
type: integer
format: int32
- name: pageToken
description: Provide this to retrieve the subsequent page.
in: query
required: false
type: string
tags:
- InboxService
/api/v1/markdown/link:metadata: /api/v1/markdown/link:metadata:
get: get:
summary: GetLinkMetadata returns metadata for a given link. summary: GetLinkMetadata returns metadata for a given link.
...@@ -937,13 +905,14 @@ paths: ...@@ -937,13 +905,14 @@ paths:
parameters: parameters:
- name: inbox.name - name: inbox.name
description: |- description: |-
The name of the inbox. The resource name of the inbox.
Format: inboxes/{id}, id is the system generated auto-incremented id. Format: inboxes/{inbox}
in: path in: path
required: true required: true
type: string type: string
pattern: inboxes/[^/]+ pattern: inboxes/[^/]+
- name: inbox - name: inbox
description: Required. The inbox to update.
in: body in: body
required: true required: true
schema: schema:
...@@ -951,20 +920,40 @@ paths: ...@@ -951,20 +920,40 @@ paths:
properties: properties:
sender: sender:
type: string type: string
title: 'Format: users/{user}' title: |-
The sender of the inbox notification.
Format: users/{user}
readOnly: true
receiver: receiver:
type: string type: string
title: 'Format: users/{user}' title: |-
The receiver of the inbox notification.
Format: users/{user}
readOnly: true
status: status:
$ref: '#/definitions/v1InboxStatus' $ref: '#/definitions/v1InboxStatus'
description: The status of the inbox notification.
createTime: createTime:
type: string type: string
format: date-time format: date-time
description: Output only. The creation timestamp.
readOnly: true
type: type:
$ref: '#/definitions/v1InboxType' $ref: '#/definitions/v1InboxType'
description: The type of the inbox notification.
readOnly: true
activityId: activityId:
type: integer type: integer
format: int32 format: int32
description: Optional. The activity ID associated with this inbox notification.
title: Required. The inbox to update.
required:
- inbox
- name: allowMissing
description: Optional. If set to true, allows updating missing fields.
in: query
required: false
type: boolean
tags: tags:
- InboxService - InboxService
/api/v1/{memo.name}: /api/v1/{memo.name}:
...@@ -1263,7 +1252,9 @@ paths: ...@@ -1263,7 +1252,9 @@ paths:
$ref: '#/definitions/googlerpcStatus' $ref: '#/definitions/googlerpcStatus'
parameters: parameters:
- name: name_4 - name: name_4
description: The name of the inbox to delete. description: |-
Required. The resource name of the inbox to delete.
Format: inboxes/{inbox}
in: path in: path
required: true required: true
type: string type: string
...@@ -1810,6 +1801,63 @@ paths: ...@@ -1810,6 +1801,63 @@ paths:
type: string type: string
tags: tags:
- UserService - UserService
/api/v1/{parent}/inboxes:
get:
summary: ListInboxes lists inboxes for a user.
operationId: InboxService_ListInboxes
responses:
"200":
description: A successful response.
schema:
$ref: '#/definitions/v1ListInboxesResponse'
default:
description: An unexpected error response.
schema:
$ref: '#/definitions/googlerpcStatus'
parameters:
- name: parent
description: |-
Required. The parent resource whose inboxes will be listed.
Format: users/{user}
in: path
required: true
type: string
pattern: users/[^/]+
- name: pageSize
description: |-
Optional. The maximum number of inboxes to return.
The service may return fewer than this value.
If unspecified, at most 50 inboxes will be returned.
The maximum value is 1000; values above 1000 will be coerced to 1000.
in: query
required: false
type: integer
format: int32
- name: pageToken
description: |-
Optional. A page token, received from a previous `ListInboxes` call.
Provide this to retrieve the subsequent page.
in: query
required: false
type: string
- name: filter
description: |-
Optional. Filter to apply to the list results.
Example: "status=UNREAD" or "type=MEMO_COMMENT"
Supported operators: =, !=
Supported fields: status, type, sender, create_time
in: query
required: false
type: string
- name: orderBy
description: |-
Optional. The order to sort results by.
Example: "create_time desc" or "status asc"
in: query
required: false
type: string
tags:
- InboxService
/api/v1/{parent}/memos: /api/v1/{parent}/memos:
get: get:
summary: ListMemos lists memos with pagination and filter. summary: ListMemos lists memos with pagination and filter.
...@@ -3170,25 +3218,37 @@ definitions: ...@@ -3170,25 +3218,37 @@ definitions:
properties: properties:
name: name:
type: string type: string
description: |- title: |-
The name of the inbox. The resource name of the inbox.
Format: inboxes/{id}, id is the system generated auto-incremented id. Format: inboxes/{inbox}
sender: sender:
type: string type: string
title: 'Format: users/{user}' title: |-
The sender of the inbox notification.
Format: users/{user}
readOnly: true
receiver: receiver:
type: string type: string
title: 'Format: users/{user}' title: |-
The receiver of the inbox notification.
Format: users/{user}
readOnly: true
status: status:
$ref: '#/definitions/v1InboxStatus' $ref: '#/definitions/v1InboxStatus'
description: The status of the inbox notification.
createTime: createTime:
type: string type: string
format: date-time format: date-time
description: Output only. The creation timestamp.
readOnly: true
type: type:
$ref: '#/definitions/v1InboxType' $ref: '#/definitions/v1InboxType'
description: The type of the inbox notification.
readOnly: true
activityId: activityId:
type: integer type: integer
format: int32 format: int32
description: Optional. The activity ID associated with this inbox notification.
v1InboxStatus: v1InboxStatus:
type: string type: string
enum: enum:
...@@ -3196,6 +3256,12 @@ definitions: ...@@ -3196,6 +3256,12 @@ definitions:
- UNREAD - UNREAD
- ARCHIVED - ARCHIVED
default: STATUS_UNSPECIFIED default: STATUS_UNSPECIFIED
description: |-
Status enumeration for inbox notifications.
- STATUS_UNSPECIFIED: Unspecified status.
- UNREAD: The notification is unread.
- ARCHIVED: The notification is archived.
v1InboxType: v1InboxType:
type: string type: string
enum: enum:
...@@ -3203,6 +3269,12 @@ definitions: ...@@ -3203,6 +3269,12 @@ definitions:
- MEMO_COMMENT - MEMO_COMMENT
- VERSION_UPDATE - VERSION_UPDATE
default: TYPE_UNSPECIFIED default: TYPE_UNSPECIFIED
description: |-
Type enumeration for inbox notifications.
- TYPE_UNSPECIFIED: Unspecified type.
- MEMO_COMMENT: Memo comment notification.
- VERSION_UPDATE: Version update notification.
v1ItalicNode: v1ItalicNode:
type: object type: object
properties: properties:
...@@ -3303,11 +3375,16 @@ definitions: ...@@ -3303,11 +3375,16 @@ definitions:
items: items:
type: object type: object
$ref: '#/definitions/v1Inbox' $ref: '#/definitions/v1Inbox'
description: The list of inboxes.
nextPageToken: nextPageToken:
type: string type: string
description: |- description: |-
A token, which can be sent as `page_token` to retrieve the next page. A token that can be sent as `page_token` to retrieve the next page.
If this field is omitted, there are no subsequent pages. If this field is omitted, there are no subsequent pages.
totalSize:
type: integer
format: int32
description: The total count of inboxes (may be approximate).
v1ListMemoAttachmentsResponse: v1ListMemoAttachmentsResponse:
type: object type: object
properties: properties:
......
...@@ -13,6 +13,8 @@ import ( ...@@ -13,6 +13,8 @@ import (
const ( const (
// DefaultPageSize is the default page size for requests. // DefaultPageSize is the default page size for requests.
DefaultPageSize = 10 DefaultPageSize = 10
// MaxPageSize is the maximum page size for requests.
MaxPageSize = 1000
) )
func convertStateFromStore(rowStatus store.RowStatus) v1pb.State { func convertStateFromStore(rowStatus store.RowStatus) v1pb.State {
......
...@@ -15,9 +15,27 @@ import ( ...@@ -15,9 +15,27 @@ import (
) )
func (s *APIV1Service) ListInboxes(ctx context.Context, request *v1pb.ListInboxesRequest) (*v1pb.ListInboxesResponse, error) { func (s *APIV1Service) ListInboxes(ctx context.Context, request *v1pb.ListInboxesRequest) (*v1pb.ListInboxesResponse, error) {
user, err := s.GetCurrentUser(ctx) // Extract user ID from parent resource name
userID, err := ExtractUserIDFromName(request.Parent)
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user") return nil, status.Errorf(codes.InvalidArgument, "invalid parent name %q: %v", request.Parent, err)
}
// Get current user for authorization
currentUser, err := s.GetCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user")
}
if currentUser == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
}
// Check if current user can access the requested user's inboxes
if currentUser.ID != userID {
// Only allow hosts and admins to access other users' inboxes
if currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin {
return nil, status.Errorf(codes.PermissionDenied, "cannot access inboxes for user %q", request.Parent)
}
} }
var limit, offset int var limit, offset int
...@@ -34,15 +52,20 @@ func (s *APIV1Service) ListInboxes(ctx context.Context, request *v1pb.ListInboxe ...@@ -34,15 +52,20 @@ func (s *APIV1Service) ListInboxes(ctx context.Context, request *v1pb.ListInboxe
if limit <= 0 { if limit <= 0 {
limit = DefaultPageSize limit = DefaultPageSize
} }
if limit > MaxPageSize {
limit = MaxPageSize
}
limitPlusOne := limit + 1 limitPlusOne := limit + 1
inboxes, err := s.Store.ListInboxes(ctx, &store.FindInbox{ findInbox := &store.FindInbox{
ReceiverID: &user.ID, ReceiverID: &userID,
Limit: &limitPlusOne, Limit: &limitPlusOne,
Offset: &offset, Offset: &offset,
}) }
inboxes, err := s.Store.ListInboxes(ctx, findInbox)
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list inbox: %v", err) return nil, status.Errorf(codes.Internal, "failed to list inboxes: %v", err)
} }
inboxMessages := []*v1pb.Inbox{} inboxMessages := []*v1pb.Inbox{}
...@@ -51,7 +74,7 @@ func (s *APIV1Service) ListInboxes(ctx context.Context, request *v1pb.ListInboxe ...@@ -51,7 +74,7 @@ func (s *APIV1Service) ListInboxes(ctx context.Context, request *v1pb.ListInboxe
inboxes = inboxes[:limit] inboxes = inboxes[:limit]
nextPageToken, err = getPageToken(limit, offset+limit) nextPageToken, err = getPageToken(limit, offset+limit)
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get next page token, error: %v", err) return nil, status.Errorf(codes.Internal, "failed to get next page token: %v", err)
} }
} }
for _, inbox := range inboxes { for _, inbox := range inboxes {
...@@ -65,6 +88,7 @@ func (s *APIV1Service) ListInboxes(ctx context.Context, request *v1pb.ListInboxe ...@@ -65,6 +88,7 @@ func (s *APIV1Service) ListInboxes(ctx context.Context, request *v1pb.ListInboxe
response := &v1pb.ListInboxesResponse{ response := &v1pb.ListInboxesResponse{
Inboxes: inboxMessages, Inboxes: inboxMessages,
NextPageToken: nextPageToken, NextPageToken: nextPageToken,
TotalSize: int32(len(inboxMessages)), // For now, use actual returned count
} }
return response, nil return response, nil
} }
...@@ -76,17 +100,46 @@ func (s *APIV1Service) UpdateInbox(ctx context.Context, request *v1pb.UpdateInbo ...@@ -76,17 +100,46 @@ func (s *APIV1Service) UpdateInbox(ctx context.Context, request *v1pb.UpdateInbo
inboxID, err := ExtractInboxIDFromName(request.Inbox.Name) inboxID, err := ExtractInboxIDFromName(request.Inbox.Name)
if err != nil { if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid inbox name: %v", err) return nil, status.Errorf(codes.InvalidArgument, "invalid inbox name %q: %v", request.Inbox.Name, err)
}
// Get current user for authorization
currentUser, err := s.GetCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user")
} }
if currentUser == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
}
// Get the existing inbox to verify ownership
inboxes, err := s.Store.ListInboxes(ctx, &store.FindInbox{
ID: &inboxID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get inbox: %v", err)
}
if len(inboxes) == 0 {
return nil, status.Errorf(codes.NotFound, "inbox %q not found", request.Inbox.Name)
}
existingInbox := inboxes[0]
// Check if current user can update this inbox (must be the receiver)
if currentUser.ID != existingInbox.ReceiverID {
return nil, status.Errorf(codes.PermissionDenied, "cannot update inbox for another user")
}
update := &store.UpdateInbox{ update := &store.UpdateInbox{
ID: inboxID, ID: inboxID,
} }
for _, field := range request.UpdateMask.Paths { for _, field := range request.UpdateMask.Paths {
if field == "status" { if field == "status" {
if request.Inbox.Status == v1pb.Inbox_STATUS_UNSPECIFIED { if request.Inbox.Status == v1pb.Inbox_STATUS_UNSPECIFIED {
return nil, status.Errorf(codes.InvalidArgument, "status is required") return nil, status.Errorf(codes.InvalidArgument, "status cannot be unspecified")
} }
update.Status = convertInboxStatusToStore(request.Inbox.Status) update.Status = convertInboxStatusToStore(request.Inbox.Status)
} else {
return nil, status.Errorf(codes.InvalidArgument, "unsupported field in update mask: %q", field)
} }
} }
...@@ -101,13 +154,39 @@ func (s *APIV1Service) UpdateInbox(ctx context.Context, request *v1pb.UpdateInbo ...@@ -101,13 +154,39 @@ func (s *APIV1Service) UpdateInbox(ctx context.Context, request *v1pb.UpdateInbo
func (s *APIV1Service) DeleteInbox(ctx context.Context, request *v1pb.DeleteInboxRequest) (*emptypb.Empty, error) { func (s *APIV1Service) DeleteInbox(ctx context.Context, request *v1pb.DeleteInboxRequest) (*emptypb.Empty, error) {
inboxID, err := ExtractInboxIDFromName(request.Name) inboxID, err := ExtractInboxIDFromName(request.Name)
if err != nil { if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid inbox name: %v", err) return nil, status.Errorf(codes.InvalidArgument, "invalid inbox name %q: %v", request.Name, err)
}
// Get current user for authorization
currentUser, err := s.GetCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user")
}
if currentUser == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
}
// Get the existing inbox to verify ownership
inboxes, err := s.Store.ListInboxes(ctx, &store.FindInbox{
ID: &inboxID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get inbox: %v", err)
}
if len(inboxes) == 0 {
return nil, status.Errorf(codes.NotFound, "inbox %q not found", request.Name)
}
existingInbox := inboxes[0]
// Check if current user can delete this inbox (must be the receiver)
if currentUser.ID != existingInbox.ReceiverID {
return nil, status.Errorf(codes.PermissionDenied, "cannot delete inbox for another user")
} }
if err := s.Store.DeleteInbox(ctx, &store.DeleteInbox{ if err := s.Store.DeleteInbox(ctx, &store.DeleteInbox{
ID: inboxID, ID: inboxID,
}); err != nil { }); err != nil {
return nil, status.Errorf(codes.Internal, "failed to update inbox: %v", err) return nil, status.Errorf(codes.Internal, "failed to delete inbox: %v", err)
} }
return &emptypb.Empty{}, nil return &emptypb.Empty{}, nil
} }
......
This diff is collapsed.
...@@ -13,14 +13,23 @@ import { useTranslate } from "@/utils/i18n"; ...@@ -13,14 +13,23 @@ import { useTranslate } from "@/utils/i18n";
const Inboxes = observer(() => { const Inboxes = observer(() => {
const t = useTranslate(); const t = useTranslate();
const { md } = useResponsiveWidth(); const { md } = useResponsiveWidth();
const inboxes = sortBy(userStore.state.inboxes, (inbox) => { const inboxes = sortBy(userStore.state.inboxes, (inbox) => {
if (inbox.status === Inbox_Status.UNREAD) return 0; if (inbox.status === Inbox_Status.UNREAD) return 0;
if (inbox.status === Inbox_Status.ARCHIVED) return 1; if (inbox.status === Inbox_Status.ARCHIVED) return 1;
return 2; return 2;
}); });
const fetchInboxes = async () => {
try {
await userStore.fetchInboxes();
} catch (error) {
console.error("Failed to fetch inboxes:", error);
}
};
useEffect(() => { useEffect(() => {
userStore.fetchInboxes(); fetchInboxes();
}, []); }, []);
return ( return (
......
...@@ -160,7 +160,14 @@ const userStore = (() => { ...@@ -160,7 +160,14 @@ const userStore = (() => {
}; };
const fetchInboxes = async () => { const fetchInboxes = async () => {
const { inboxes } = await inboxServiceClient.listInboxes({}); if (!state.currentUser) {
throw new Error("No current user available");
}
const { inboxes } = await inboxServiceClient.listInboxes({
parent: state.currentUser,
});
state.setPartial({ state.setPartial({
inboxes, inboxes,
}); });
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment