diff --git a/middleware/auth.go b/middleware/auth.go index a0ee0f2..d21434c 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -108,4 +108,3 @@ func Auth(acceptableRoles ...Role) func(c *gin.Context) { Build()) } } - diff --git a/middleware/event_action.go b/middleware/event_action.go index 223a4c3..8638ada 100644 --- a/middleware/event_action.go +++ b/middleware/event_action.go @@ -31,7 +31,7 @@ func GetClientFromContext(c *gin.Context) (clientId int64, err error) { } /* -Get event and put to context. +Get event and put to context. This middleware should be added to any route that performs event action. */ func EventActionPreProcess(c *gin.Context) { diff --git a/middleware/huma_auth.go b/middleware/huma_auth.go index 5e7bdd0..2523cbf 100644 --- a/middleware/huma_auth.go +++ b/middleware/huma_auth.go @@ -109,7 +109,7 @@ func authenticateToken(authHeader string) (*AuthContext, error) { if err != nil || !tokenParsed.Valid { return nil, err } - + return &AuthContext{ ID: claims.Who, Member: claims.Member, @@ -153,7 +153,7 @@ func authenticateToken(authHeader string) (*AuthContext, error) { Role: userRoles, UserInfo: userinfo, } - + return &AuthContext{ User: user, ID: member.MemberId, @@ -196,4 +196,4 @@ func AuthenticateUserWithContext(ctx context.Context, authHeader string, accepta // Fallback to original implementation for backward compatibility return AuthenticateUser(authHeader, acceptableRoles...) -} \ No newline at end of file +} diff --git a/middleware/huma_auth_test.go b/middleware/huma_auth_test.go index c420b2f..4b6b2e3 100644 --- a/middleware/huma_auth_test.go +++ b/middleware/huma_auth_test.go @@ -183,7 +183,7 @@ func TestHumaAuthMiddleware(t *testing.T) { ShouldFail: false, }, { - CaseID: "1.2", + CaseID: "1.2", Description: "Valid legacy JWT token with admin role", Authorization: util.GenToken("admin", "2333333333"), ExpectedAuth: true, @@ -253,21 +253,21 @@ func TestHumaAuthMiddleware(t *testing.T) { if capturedCtx != nil { authCtx = middleware.GetAuthContextFromHuma(capturedCtx) } - + if tc.ExpectedAuth { if authCtx == nil { t.Errorf("Expected auth context to be set for case %s", tc.CaseID) return } - + if authCtx.Role != tc.ExpectedRole { t.Errorf("Expected role %s, got %s", tc.ExpectedRole, authCtx.Role) } - + if authCtx.ID == "" { t.Error("Expected user ID to be set") } - + if !authCtx.IsLegacyJWT { t.Error("Expected legacy JWT flag to be true") } @@ -302,7 +302,7 @@ func TestRequireAuthMiddleware(t *testing.T) { ExpectedContinue: true, }, { - CaseID: "2.2", + CaseID: "2.2", Description: "Member access with member or admin role required", AuthContext: &middleware.AuthContext{ ID: "member123", @@ -326,7 +326,7 @@ func TestRequireAuthMiddleware(t *testing.T) { Description: "Client access with admin role required", AuthContext: &middleware.AuthContext{ ID: "client123", - Role: "client", + Role: "client", IsLegacyJWT: true, }, RequiredRoles: []middleware.Role{"admin"}, @@ -351,7 +351,7 @@ func TestRequireAuthMiddleware(t *testing.T) { t.Run(tc.CaseID+"_"+tc.Description, func(t *testing.T) { // Create mock context mockCtx := NewMockHumaContext() - + // Set auth context if provided if tc.AuthContext != nil { mockCtx.ctx = context.WithValue(mockCtx.ctx, middleware.GetAuthContextKey(), tc.AuthContext) @@ -408,7 +408,7 @@ func TestGetAuthContextFromHuma(t *testing.T) { for _, tc := range testCases { t.Run(tc.CaseID+"_"+tc.Description, func(t *testing.T) { mockCtx := NewMockHumaContext() - + if tc.SetContext && tc.AuthContext != nil { mockCtx.ctx = context.WithValue(mockCtx.ctx, middleware.GetAuthContextKey(), tc.AuthContext) } @@ -484,7 +484,7 @@ func TestAuthenticateUserWithContext(t *testing.T) { for _, tc := range testCases { t.Run(tc.CaseID+"_"+tc.Description, func(t *testing.T) { ctx := context.Background() - + if tc.ContextAuth != nil { ctx = context.WithValue(ctx, middleware.GetAuthContextKey(), tc.ContextAuth) } @@ -510,4 +510,4 @@ func TestAuthenticateUserWithContext(t *testing.T) { } }) } -} \ No newline at end of file +} diff --git a/middleware/huma_helpers.go b/middleware/huma_helpers.go index 86abcdf..a648e59 100644 --- a/middleware/huma_helpers.go +++ b/middleware/huma_helpers.go @@ -38,7 +38,7 @@ func AuthenticateUser(authHeader string, acceptableRoles ...Role) (*AuthContext, if err != nil || !tokenParsed.Valid { return nil, huma.Error401Unauthorized("not authorized, token not valid") } - + for _, roleObj := range acceptableRoles { if string(roleObj) == claims.Role { return &AuthContext{ @@ -90,7 +90,7 @@ func AuthenticateUser(authHeader string, acceptableRoles ...Role) (*AuthContext, Role: userRoles, UserInfo: userinfo, } - + return &AuthContext{ User: user, ID: member.MemberId, @@ -137,11 +137,11 @@ func CreateIdentityFromAuth(auth *AuthContext) model.Identity { Id: auth.ID, Role: auth.Role, } - + if member, ok := auth.Member.(model.Member); ok { identity.Member = member } - + return identity } @@ -167,4 +167,4 @@ func GetClientIdFromAuthContext(ctx context.Context) (int64, error) { func MustGetEventContext(ctx context.Context) struct{ Event model.Event } { // This function is not used in the new approach panic("MustGetEventContext should not be called in new Huma operations") -} \ No newline at end of file +} diff --git a/middleware/huma_logger.go b/middleware/huma_logger.go index 7481a4f..268e180 100644 --- a/middleware/huma_logger.go +++ b/middleware/huma_logger.go @@ -12,25 +12,25 @@ import ( func HumaLogger() func(ctx huma.Context, next func(huma.Context)) { return func(ctx huma.Context, next func(huma.Context)) { startTime := time.Now() - + // Process request next(ctx) - + endTime := time.Now() latencyTime := endTime.Sub(startTime).Microseconds() - + // Extract user ID from auth context if available var userID string if auth := GetAuthContext(ctx.Context()); auth != nil { userID = auth.ID } - + // Get response status from context status := ctx.Status() if status == 0 { status = 200 // Default to 200 if not set } - + util.Logger.WithFields(logrus.Fields{ "status_code": status, "latency": latencyTime, @@ -43,4 +43,4 @@ func HumaLogger() func(ctx huma.Context, next func(huma.Context)) { "userId": userID, }).Info("HTTP Request") } -} \ No newline at end of file +} diff --git a/middleware/logger.go b/middleware/logger.go index 247da9a..efc7385 100644 --- a/middleware/logger.go +++ b/middleware/logger.go @@ -15,7 +15,7 @@ func Logger(c *gin.Context) { endTime := time.Now() - latencyTime := endTime.Sub(startTime).Microseconds() + latencyTime := endTime.Sub(startTime).Microseconds() util.Logger.WithFields(logrus.Fields{ "status_code": c.Writer.Status(), diff --git a/migrations/000007_optional_client_id.down.sql b/migrations/000007_optional_client_id.down.sql new file mode 100644 index 0000000..b1360e2 --- /dev/null +++ b/migrations/000007_optional_client_id.down.sql @@ -0,0 +1,17 @@ +-- Rollback: Make client_id mandatory again in event table +-- This rollback will fail if there are any events with NULL client_id + +-- Remove the indexes we added +DROP INDEX IF EXISTS public.event_anonymous_idx; +DROP INDEX IF EXISTS public.event_client_id_not_null_idx; + +-- Drop the modified foreign key constraint +ALTER TABLE public.event DROP CONSTRAINT event_client_id_fkey; + +-- Make client_id NOT NULL again (this will fail if there are NULL values) +ALTER TABLE public.event ALTER COLUMN client_id SET NOT NULL; + +-- Recreate the original foreign key constraint +ALTER TABLE public.event ADD CONSTRAINT event_client_id_fkey + FOREIGN KEY (client_id) REFERENCES public.client(client_id) + ON UPDATE RESTRICT ON DELETE RESTRICT; \ No newline at end of file diff --git a/migrations/000007_optional_client_id.up.sql b/migrations/000007_optional_client_id.up.sql new file mode 100644 index 0000000..744d62e --- /dev/null +++ b/migrations/000007_optional_client_id.up.sql @@ -0,0 +1,19 @@ +-- Make client_id nullable in event table to allow anonymous event creation +-- Related to: https://github.com/nbtca/Saturday/issues/192 + +-- Drop existing foreign key constraint +ALTER TABLE public.event DROP CONSTRAINT event_client_id_fkey; + +-- Make client_id nullable +ALTER TABLE public.event ALTER COLUMN client_id DROP NOT NULL; + +-- Recreate foreign key constraint allowing NULL values +ALTER TABLE public.event ADD CONSTRAINT event_client_id_fkey + FOREIGN KEY (client_id) REFERENCES public.client(client_id) + ON UPDATE RESTRICT ON DELETE SET NULL; + +-- Add index for efficient querying of anonymous events (where client_id IS NULL) +CREATE INDEX event_anonymous_idx ON public.event (client_id) WHERE client_id IS NULL; + +-- Add index for better performance on non-null client_id queries +CREATE INDEX event_client_id_not_null_idx ON public.event (client_id) WHERE client_id IS NOT NULL; \ No newline at end of file diff --git a/model/client.go b/model/client.go index 99afaf8..592f71f 100644 --- a/model/client.go +++ b/model/client.go @@ -3,7 +3,7 @@ package model type Client struct { ClientId int64 `json:"clientId" db:"client_id"` OpenId string `json:"openid"` - LogtoId string `json:"logtoId" db:"logto_id"` + LogtoId string `json:"logtoId" db:"logto_id"` GmtCreate string `json:"gmtCreate" db:"gmt_create"` GmtModified string `json:"gmtModified" db:"gmt_modified"` } diff --git a/model/dto/event.go b/model/dto/event.go index 8ec1665..f475951 100644 --- a/model/dto/event.go +++ b/model/dto/event.go @@ -6,12 +6,12 @@ type EventID struct { type CommitRequest struct { Content string `json:"content"` - Size string `json:"size"` + Size string `json:"size"` } type AlterCommitRequest struct { Content string `json:"content"` - Size string `json:"size"` + Size string `json:"size"` } type UpdateRequest struct { diff --git a/model/event.go b/model/event.go index 7035046..cbedec1 100644 --- a/model/event.go +++ b/model/event.go @@ -10,7 +10,7 @@ import ( type Event struct { EventId int64 `json:"eventId" db:"event_id"` - ClientId int64 `json:"clientId" db:"client_id"` + ClientId sql.NullInt64 `json:"clientId,omitempty" db:"client_id"` GithubIssueId sql.NullInt64 `json:"githubIssueId" db:"github_issue_id"` GithubIssueNumber sql.NullInt64 `json:"githubIssueNumber" db:"github_issue_number"` Model string `json:"model"` @@ -69,7 +69,7 @@ type EventAction struct { type PublicEvent struct { EventId int64 `json:"eventId" db:"event_id"` - ClientId int64 `json:"clientId" db:"client_id"` + ClientId sql.NullInt64 `json:"clientId,omitempty" db:"client_id"` Model string `json:"model"` Problem string `json:"problem" db:"event_description"` MemberId string `json:"-" db:"member_id"` @@ -140,3 +140,11 @@ type EventExportedGroupedByMember struct { MemberPhone string Hour float64 } + +type CreateAnonymousEventRequest struct { + Model string `json:"model" validate:"omitempty,max=40"` + Phone string `json:"phone" validate:"required,len=11,numeric"` + QQ string `json:"qq" validate:"omitempty,min=5,max=20,numeric"` + ContactPreference string `json:"contactPreference" validate:"required,oneof=phone qq"` + Problem string `json:"problem" validate:"required,max=1000"` +} diff --git a/model/setting.go b/model/setting.go index 9652eaf..c0fa246 100644 --- a/model/setting.go +++ b/model/setting.go @@ -2,4 +2,4 @@ package model type Setting struct { Setting string `json:"setting"` -} \ No newline at end of file +} diff --git a/repo/client.go b/repo/client.go index 11c351b..c36d3ce 100644 --- a/repo/client.go +++ b/repo/client.go @@ -34,7 +34,6 @@ func GetClientByLogtoId(logtoId string) (model.Client, error) { return client, nil } - func CreateClient(client *model.Client) error { client.GmtCreate = util.GetDate() client.GmtModified = util.GetDate() diff --git a/repo/event.go b/repo/event.go index 38d376c..2c924ee 100644 --- a/repo/event.go +++ b/repo/event.go @@ -307,12 +307,12 @@ func SetEventStatus(eventId int64, status string, conn *sqlx.Tx) (sql.Result, er return conn.Exec(db.Rebind(sql), eventId, status, status) } -func GetEventClientId(eventId int64) (int64, error) { - var clientId int64 - sql, args, _ := sq.Select("client_id").From("event").Where(squirrel.Eq{"event_id": eventId}).ToSql() - err := db.Get(&clientId, sql, args...) +func GetEventClientId(eventId int64) (sql.NullInt64, error) { + var clientId sql.NullInt64 + sqlStr, args, _ := sq.Select("client_id").From("event").Where(squirrel.Eq{"event_id": eventId}).ToSql() + err := db.Get(&clientId, sqlStr, args...) if err != nil { - return 0, err + return sql.NullInt64{}, err } return clientId, nil } diff --git a/router/client.go b/router/client.go index a700841..0a38867 100644 --- a/router/client.go +++ b/router/client.go @@ -47,7 +47,7 @@ func (ClientRouter) CreateTokenViaLogto(ctx context.Context, input *CreateTokenV if err != nil { return nil, err } - + logtoId := auth.User.UserInfo.Sub if logtoId == "" { return nil, huma.Error422UnprocessableEntity("user not found") @@ -62,7 +62,7 @@ func (ClientRouter) CreateTokenViaLogto(ctx context.Context, input *CreateTokenV if err != nil { return nil, huma.Error422UnprocessableEntity(err.Error()) } - + response := dto.ClientTokenResponse{ Token: token, Client: client, diff --git a/router/event.go b/router/event.go index 6b6c732..f3fe28b 100644 --- a/router/event.go +++ b/router/event.go @@ -3,6 +3,7 @@ package router import ( "bytes" "context" + "database/sql" "fmt" "time" @@ -51,21 +52,23 @@ func (er EventRouter) GetEventById(ctx context.Context, input *GetEventByIdInput if err != nil { return nil, err } - + event, err := service.EventServiceApp.GetEventById(input.EventId) if err != nil { return nil, huma.Error422UnprocessableEntity(err.Error()) } - + clientId, err := middleware.GetClientIdFromAuth(auth) if err != nil { return nil, err } - if event.MemberId != auth.ID && event.ClientId != clientId { + // Check authorization: either member owns the event or client owns it (if event has a client) + clientOwnsEvent := event.ClientId.Valid && event.ClientId.Int64 == clientId + if event.MemberId != auth.ID && !clientOwnsEvent { return nil, huma.Error401Unauthorized("not authorized") } - + return util.MakeCommonResponse(event), nil } @@ -94,7 +97,7 @@ func (EventRouter) ExportEventsToXlsx(ctx context.Context, input *ExportEventsTo if err != nil { return nil, err } - + f, err := service.EventServiceApp.ExportEventToXlsx(repo.EventFilter{ Offset: input.Offset, Limit: input.Limit, @@ -104,7 +107,7 @@ func (EventRouter) ExportEventsToXlsx(ctx context.Context, input *ExportEventsTo if err != nil { return nil, huma.Error422UnprocessableEntity(err.Error()) } - + formatDate := func(date string) string { t, err := time.Parse("2006-01-02T15:04:05Z", date) if err != nil { @@ -114,13 +117,13 @@ func (EventRouter) ExportEventsToXlsx(ctx context.Context, input *ExportEventsTo } filename := fmt.Sprintf("events_%s_to_%s.xlsx", formatDate(input.StartTime), formatDate(input.EndTime)) - + // Create a buffer to write the Excel file var buf bytes.Buffer if err := f.Write(&buf); err != nil { return nil, huma.Error500InternalServerError("Failed to generate Excel file") } - + return &huma.StreamResponse{ Body: func(ctx huma.Context) { w := ctx.BodyWriter() @@ -137,12 +140,12 @@ func (EventRouter) Accept(ctx context.Context, input *AcceptEventInput) (*util.C if err != nil { return nil, err } - + event, err := middleware.LoadEvent(input.EventId) if err != nil { return nil, err } - + identity := middleware.CreateIdentityFromAuth(auth) if err := service.EventServiceApp.Accept(&event, identity); err != nil { return nil, huma.Error422UnprocessableEntity(err.Error()) @@ -155,12 +158,12 @@ func (EventRouter) Drop(ctx context.Context, input *DropEventInput) (*util.Commo if err != nil { return nil, err } - + event, err := middleware.LoadEvent(input.EventId) if err != nil { return nil, err } - + identity := middleware.CreateIdentityFromAuth(auth) if err := service.EventServiceApp.Act(&event, identity, util.Drop); err != nil { return nil, huma.Error422UnprocessableEntity(err.Error()) @@ -173,12 +176,12 @@ func (EventRouter) Commit(ctx context.Context, input *CommitEventInput) (*util.C if err != nil { return nil, err } - + event, err := middleware.LoadEvent(input.EventId) if err != nil { return nil, err } - + identity := middleware.CreateIdentityFromAuth(auth) if input.Body.Size != "" { event.Size = input.Body.Size @@ -194,12 +197,12 @@ func (EventRouter) AlterCommit(ctx context.Context, input *AlterCommitEventInput if err != nil { return nil, err } - + event, err := middleware.LoadEvent(input.EventId) if err != nil { return nil, err } - + identity := middleware.CreateIdentityFromAuth(auth) if input.Body.Size != "" { event.Size = input.Body.Size @@ -215,12 +218,12 @@ func (EventRouter) RejectCommit(ctx context.Context, input *RejectCommitEventInp if err != nil { return nil, err } - + event, err := middleware.LoadEvent(input.EventId) if err != nil { return nil, err } - + identity := middleware.CreateIdentityFromAuth(auth) if err := service.EventServiceApp.Act(&event, identity, util.Reject); err != nil { return nil, huma.Error422UnprocessableEntity(err.Error()) @@ -233,12 +236,12 @@ func (EventRouter) Close(ctx context.Context, input *CloseEventInput) (*util.Com if err != nil { return nil, err } - + event, err := middleware.LoadEvent(input.EventId) if err != nil { return nil, err } - + identity := middleware.CreateIdentityFromAuth(auth) if err := service.EventServiceApp.Act(&event, identity, util.Close); err != nil { return nil, huma.Error422UnprocessableEntity(err.Error()) @@ -251,7 +254,7 @@ func (er EventRouter) GetClientEventByPage(ctx context.Context, input *GetClient if err != nil { return nil, err } - + clientId, err := middleware.GetClientIdFromAuth(auth) if err != nil { return nil, err @@ -273,14 +276,14 @@ func (EventRouter) Create(ctx context.Context, input *CreateClientEventInput) (* if err != nil { return nil, err } - + clientId, err := middleware.GetClientIdFromAuth(auth) if err != nil { return nil, err } - + event := &model.Event{ - ClientId: clientId, + ClientId: sql.NullInt64{Valid: true, Int64: clientId}, Model: input.Body.Model, Phone: input.Body.Phone, QQ: input.Body.QQ, @@ -299,17 +302,17 @@ func (er EventRouter) Update(ctx context.Context, input *UpdateClientEventInput) if err != nil { return nil, err } - + event, err := middleware.LoadEvent(input.EventId) if err != nil { return nil, err } - + clientId, err := middleware.GetClientIdFromAuth(auth) if err != nil { return nil, err } - + identity := middleware.CreateIdentityFromAuth(auth) identity.ClientId = clientId @@ -342,17 +345,17 @@ func (er EventRouter) Cancel(ctx context.Context, input *CancelClientEventInput) if err != nil { return nil, err } - + event, err := middleware.LoadEvent(input.EventId) if err != nil { return nil, err } - + clientId, err := middleware.GetClientIdFromAuth(auth) if err != nil { return nil, err } - + identity := middleware.CreateIdentityFromAuth(auth) identity.ClientId = clientId @@ -362,5 +365,21 @@ func (er EventRouter) Cancel(ctx context.Context, input *CancelClientEventInput) return util.MakeCommonResponse(event), nil } +func (EventRouter) CreateAnonymous(ctx context.Context, input *CreateAnonymousEventInput) (*util.CommonResponse[model.Event], error) { + event := &model.Event{ + ClientId: sql.NullInt64{Valid: false}, + Model: input.Body.Model, + Phone: input.Body.Phone, + QQ: input.Body.QQ, + ContactPreference: input.Body.ContactPreference, + Problem: input.Body.Problem, + } + + err := service.EventServiceApp.CreateAnonymousEvent(event) + if err != nil { + return nil, huma.Error422UnprocessableEntity(err.Error()) + } + return util.MakeCommonResponse(*event), nil +} var EventRouterApp = EventRouter{} diff --git a/router/input_types.go b/router/input_types.go index 2626764..0d2bb54 100644 --- a/router/input_types.go +++ b/router/input_types.go @@ -185,6 +185,17 @@ type CancelClientEventInput struct { EventPathInput } +// Anonymous event creation (no authentication required) +type CreateAnonymousEventInput struct { + Body struct { + Model string `json:"model" validate:"omitempty,max=40" doc:"Device model"` + Phone string `json:"phone" validate:"required,len=11,numeric" doc:"Phone number (11 digits)"` + QQ string `json:"qq" validate:"omitempty,min=5,max=20,numeric" doc:"QQ number"` + ContactPreference string `json:"contactPreference" validate:"required,oneof=phone qq" doc:"Preferred contact method"` + Problem string `json:"problem" validate:"required,max=1000" doc:"Problem description"` + } +} + // Client auth endpoint inputs type CreateTokenViaLogtoInput struct { @@ -206,4 +217,4 @@ type GithubWebhookInput struct { type LogtoWebhookInput struct { // Special handling for webhooks -} \ No newline at end of file +} diff --git a/router/main.go b/router/main.go index 1219e70..feae63f 100644 --- a/router/main.go +++ b/router/main.go @@ -155,6 +155,14 @@ func SetupRouter() *chi.Mux { Tags: []string{"Event", "Public"}, }, EventRouterApp.GetPublicEventByPage) + huma.Register(api, huma.Operation{ + OperationID: "create-anonymous-event", + Method: http.MethodPost, + Path: "/events/anonymous", + Summary: "Create anonymous event (no authentication required)", + Tags: []string{"Event", "Public"}, + }, EventRouterApp.CreateAnonymous) + // Client authenticated endpoints huma.Register(api, huma.Operation{ OperationID: "create-token-via-logto", diff --git a/service/event.go b/service/event.go index c07c813..c780db2 100644 --- a/service/event.go +++ b/service/event.go @@ -80,7 +80,7 @@ func (service EventService) ExportEventToXlsx(f repo.EventFilter, startTime, end if sizeHour > 0 { group.Hour += sizeHour } else { - group.Hour += 0.5 // Default increment for unknown sizes + group.Hour += 0.5 // Default increment for unknown sizes } if group.Hour > MaxHour { group.Hour = MaxHour // Cap the hour count at MaxHour @@ -190,8 +190,8 @@ func (service EventService) CreateEvent(event *model.Event) error { return err } identity := model.Identity{ - Id: fmt.Sprint(event.ClientId), - ClientId: event.ClientId, + Id: fmt.Sprint(event.ClientId.Int64), + ClientId: event.ClientId.Int64, Role: "client", } // insert event status and event log @@ -201,6 +201,24 @@ func (service EventService) CreateEvent(event *model.Event) error { return nil } +func (service EventService) CreateAnonymousEvent(event *model.Event) error { + // insert event + if err := repo.CreateEvent(event); err != nil { + return err + } + // Create anonymous identity using contact info + identity := model.Identity{ + Id: fmt.Sprintf("anonymous-%s", event.Phone), // Use phone as unique identifier + ClientId: 0, // No client association + Role: "anonymous", + } + // insert event status and event log + if err := service.Act(event, identity, util.Create); err != nil { + return err + } + return nil +} + func (service EventService) Accept(event *model.Event, identity model.Identity) error { if err := service.Act(event, identity, util.Accept); err != nil { return err diff --git a/service/logto.go b/service/logto.go index e98029a..c37718c 100644 --- a/service/logto.go +++ b/service/logto.go @@ -365,4 +365,4 @@ func (l LogtoService) FetchUserInfo(accessToken string) (FetchUserInfoResponse, } -var LogtoServiceApp LogtoService \ No newline at end of file +var LogtoServiceApp LogtoService diff --git a/util/event-action.go b/util/event-action.go index 7824344..d4b2eae 100644 --- a/util/event-action.go +++ b/util/event-action.go @@ -153,7 +153,8 @@ func (eh *eventActionHandler) ValidateAction() error { if eh.actor.Id == eh.event.MemberId { roles = append(roles, "member_current") } - if eh.actor.ClientId == eh.event.ClientId { + // Check if actor is the client owner of this event (handle nullable client_id) + if eh.event.ClientId.Valid && eh.actor.ClientId == eh.event.ClientId.Int64 { roles = append(roles, "client_current") } if len(eh.role) != 0 {