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_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/000008_add_images_to_event.down.sql b/migrations/000008_add_images_to_event.down.sql new file mode 100644 index 0000000..1178511 --- /dev/null +++ b/migrations/000008_add_images_to_event.down.sql @@ -0,0 +1,40 @@ +-- Recreate event_log_view without images column +DROP VIEW public.event_log_view; +CREATE VIEW public.event_log_view AS + SELECT event_log.event_log_id, + event_log.event_id, + event_log.description, + event_log.member_id, + event_log.gmt_create, + event_action.action + FROM ((public.event_log + LEFT JOIN public.event_event_action_relation ON ((event_log.event_log_id = event_event_action_relation.event_log_id))) + LEFT JOIN public.event_action ON ((event_event_action_relation.event_action_id = event_action.event_action_id))); + +-- Recreate event_view without images column +DROP VIEW public.event_view; +CREATE VIEW public.event_view AS + SELECT event.event_id, + event.client_id, + event.model, + event.phone, + event.qq, + event.contact_preference, + event.problem, + event.member_id, + event.closed_by, + event.gmt_create, + event.gmt_modified, + event.size, + COALESCE(event_status.status, ''::character varying) AS status, + event.github_issue_id, + event.github_issue_number + FROM ((public.event + LEFT JOIN public.event_event_status_relation ON ((event.event_id = event_event_status_relation.event_id))) + LEFT JOIN public.event_status ON ((event_event_status_relation.event_status_id = event_status.event_status_id))); + +-- Remove images field from event_log table +ALTER TABLE event_log DROP COLUMN images; + +-- Remove images field from event table +ALTER TABLE event DROP COLUMN images; diff --git a/migrations/000008_add_images_to_event.up.sql b/migrations/000008_add_images_to_event.up.sql new file mode 100644 index 0000000..ebcfff0 --- /dev/null +++ b/migrations/000008_add_images_to_event.up.sql @@ -0,0 +1,44 @@ +-- Add images field to event table (client's problem images) +ALTER TABLE event ADD COLUMN images TEXT; +COMMENT ON COLUMN event.images IS '事件图片(JSON数组)'; + +-- Add images field to event_log table (member's repair images) +ALTER TABLE event_log ADD COLUMN images TEXT; +COMMENT ON COLUMN event_log.images IS '维修记录图片(JSON数组)'; + +-- Recreate event_view to include the new images column +DROP VIEW public.event_view; +CREATE VIEW public.event_view AS + SELECT event.event_id, + event.client_id, + event.model, + event.phone, + event.qq, + event.contact_preference, + event.problem, + event.images, + event.member_id, + event.closed_by, + event.gmt_create, + event.gmt_modified, + event.size, + COALESCE(event_status.status, ''::character varying) AS status, + event.github_issue_id, + event.github_issue_number + FROM ((public.event + LEFT JOIN public.event_event_status_relation ON ((event.event_id = event_event_status_relation.event_id))) + LEFT JOIN public.event_status ON ((event_event_status_relation.event_status_id = event_status.event_status_id))); + +-- Recreate event_log_view to include the new images column +DROP VIEW public.event_log_view; +CREATE VIEW public.event_log_view AS + SELECT event_log.event_log_id, + event_log.event_id, + event_log.description, + event_log.images, + event_log.member_id, + event_log.gmt_create, + event_action.action + FROM ((public.event_log + LEFT JOIN public.event_event_action_relation ON ((event_log.event_log_id = event_event_action_relation.event_log_id))) + LEFT JOIN public.event_action ON ((event_event_action_relation.event_action_id = event_action.event_action_id))); diff --git a/model/db_types.go b/model/db_types.go new file mode 100644 index 0000000..1e051d2 --- /dev/null +++ b/model/db_types.go @@ -0,0 +1,48 @@ +package model + +import ( + "database/sql/driver" + "encoding/json" + "fmt" +) + +// StringSlice is a custom type for []string that can be scanned from JSON in database +type StringSlice []string + +// Scan implements sql.Scanner interface +func (s *StringSlice) Scan(value interface{}) error { + if value == nil { + *s = []string{} + return nil + } + + var bytes []byte + switch v := value.(type) { + case []byte: + bytes = v + case string: + bytes = []byte(v) + default: + return fmt.Errorf("unsupported type for StringSlice: %T", value) + } + + // Handle empty or null JSON + if len(bytes) == 0 || string(bytes) == "null" { + *s = []string{} + return nil + } + + return json.Unmarshal(bytes, s) +} + +// Value implements driver.Valuer interface +func (s StringSlice) Value() (driver.Value, error) { + if s == nil { + return "[]", nil + } + bytes, err := json.Marshal(s) + if err != nil { + return nil, err + } + return string(bytes), nil +} diff --git a/model/dto/event.go b/model/dto/event.go index f475951..ba1f4ce 100644 --- a/model/dto/event.go +++ b/model/dto/event.go @@ -5,28 +5,32 @@ type EventID struct { } type CommitRequest struct { - Content string `json:"content"` - Size string `json:"size"` + Content string `json:"content"` + Size string `json:"size"` + Images []string `json:"images,omitempty"` } type AlterCommitRequest struct { - Content string `json:"content"` - Size string `json:"size"` + Content string `json:"content"` + Size string `json:"size"` + Images []string `json:"images,omitempty"` } type UpdateRequest struct { - Phone string `json:"phone" binding:"omitempty,len=11,numeric"` - QQ string `json:"qq" binding:"omitempty,min=5,max=20,numeric"` - Problem string `json:"problem" db:"problem" binding:"omitempty,max=1000"` - Model string `json:"model" binding:"omitempty,max=40"` - ContactPreference string `json:"contactPreference" db:"contact_preference" ` - Size string `json:"size" db:"size" ` + Phone string `json:"phone" binding:"omitempty,len=11,numeric"` + QQ string `json:"qq" binding:"omitempty,min=5,max=20,numeric"` + Problem string `json:"problem" db:"problem" binding:"omitempty,max=1000"` + Images []string `json:"images,omitempty"` + Model string `json:"model" binding:"omitempty,max=40"` + ContactPreference string `json:"contactPreference" db:"contact_preference" ` + Size string `json:"size" db:"size" ` } type CreateEventRequest struct { - ClientId int64 `json:"clientId" db:"client_id"` - Model string `json:"model" binding:"omitempty,max=40"` - Phone string `json:"phone" binding:"required,omitempty,len=11,numeric"` - QQ string `json:"qq" binding:"omitempty,min=5,max=20,numeric"` - ContactPreference string `json:"contactPreference" db:"contact_preference" ` - Problem string `json:"problem" db:"problem" binding:"required,omitempty,max=1000"` + ClientId int64 `json:"clientId" db:"client_id"` + Model string `json:"model" binding:"omitempty,max=40"` + Phone string `json:"phone" binding:"required,omitempty,len=11,numeric"` + QQ string `json:"qq" binding:"omitempty,min=5,max=20,numeric"` + ContactPreference string `json:"contactPreference" db:"contact_preference" ` + Problem string `json:"problem" db:"problem" binding:"required,omitempty,max=1000"` + Images []string `json:"images,omitempty"` } diff --git a/model/event.go b/model/event.go index 7035046..0827693 100644 --- a/model/event.go +++ b/model/event.go @@ -18,6 +18,7 @@ type Event struct { QQ string `json:"qq"` ContactPreference string `json:"contactPreference" db:"contact_preference" ` Problem string `json:"problem" db:"problem"` + Images StringSlice `json:"images,omitempty" db:"images"` MemberId string `json:"memberId" db:"member_id"` Member *PublicMember `json:"member" db:"-"` ClosedBy string `json:"closedById" db:"closed_by"` @@ -49,12 +50,13 @@ type EventEventStatusRelation struct { } type EventLog struct { - EventLogId int64 `json:"logId" db:"event_log_id"` - EventId int64 `json:"-" db:"-"` - Description string `json:"description"` - MemberId string `json:"memberId" db:"member_id"` - Action string `json:"action"` - GmtCreate string `json:"gmtCreate" db:"gmt_create"` + EventLogId int64 `json:"logId" db:"event_log_id"` + EventId int64 `json:"-" db:"-"` + Description string `json:"description"` + Images StringSlice `json:"images,omitempty" db:"images"` + MemberId string `json:"memberId" db:"member_id"` + Action string `json:"action"` + GmtCreate string `json:"gmtCreate" db:"gmt_create"` } type EventActionRelation struct { @@ -72,6 +74,7 @@ type PublicEvent struct { ClientId int64 `json:"clientId" db:"client_id"` Model string `json:"model"` Problem string `json:"problem" db:"event_description"` + Images StringSlice `json:"images,omitempty" db:"images"` MemberId string `json:"-" db:"member_id"` Member *PublicMember `json:"member"` ClosedBy string `json:"-" db:"closed_by"` @@ -91,6 +94,7 @@ func CreatePublicEvent(e Event) PublicEvent { ClientId: e.ClientId, Model: e.Model, Problem: e.Problem, + Images: e.Images, MemberId: e.MemberId, Member: e.Member, ClosedBy: e.ClosedBy, diff --git a/repo/event.go b/repo/event.go index 530bfba..6e18c94 100644 --- a/repo/event.go +++ b/repo/event.go @@ -11,9 +11,9 @@ import ( ) var eventFields = []string{"event_id", "client_id", "model", "phone", "qq", "contact_preference", - "problem", "member_id", "closed_by", "status", "size", "gmt_create", "gmt_modified", "status", "github_issue_id", "github_issue_number"} + "problem", "images", "member_id", "closed_by", "status", "size", "gmt_create", "gmt_modified", "status", "github_issue_id", "github_issue_number"} -var EventLogFields = []string{"event_log_id", "description", "gmt_create", "member_id", "action"} +var EventLogFields = []string{"event_log_id", "description", "images", "gmt_create", "member_id", "action"} func getEventStatement() squirrel.SelectBuilder { prefixedMember := util.Prefixer("member", memberFields) @@ -204,6 +204,7 @@ func UpdateEvent(event *model.Event, eventLog *model.EventLog) error { Set("qq", event.QQ). Set("contact_preference", event.ContactPreference). Set("problem", event.Problem). + Set("images", event.Images). Set("size", event.Size). Set("member_id", event.MemberId). Set("closed_by", event.ClosedBy) @@ -238,12 +239,13 @@ func UpdateEvent(event *model.Event, eventLog *model.EventLog) error { func CreateEvent(event *model.Event) error { event.GmtCreate = util.GetDate() event.GmtModified = util.GetDate() + createEventSql, args, _ := sq.Insert("event").Columns( "client_id", "model", "phone", "qq", - "contact_preference", "problem", "member_id", "closed_by", + "contact_preference", "problem", "images", "member_id", "closed_by", "gmt_create", "gmt_modified").Values( event.ClientId, event.Model, event.Phone, event.QQ, - event.ContactPreference, event.Problem, event.MemberId, event.ClosedBy, + event.ContactPreference, event.Problem, event.Images, event.MemberId, event.ClosedBy, event.GmtCreate, event.GmtModified).ToSql() conn, err := db.Beginx() if err != nil { @@ -265,8 +267,8 @@ func CreateEvent(event *model.Event) error { } func CreateEventLog(eventLog *model.EventLog, conn *sqlx.Tx) error { - sql, args, _ := sq.Insert("event_log").Columns("event_id", "description", "member_id", "gmt_create"). - Values(eventLog.EventId, eventLog.Description, eventLog.MemberId, util.GetDate()).ToSql() + sql, args, _ := sq.Insert("event_log").Columns("event_id", "description", "images", "member_id", "gmt_create"). + Values(eventLog.EventId, eventLog.Description, eventLog.Images, eventLog.MemberId, util.GetDate()).ToSql() var eventLogId int64 err := db.QueryRow(sql+"RETURNING event_log_id", args...).Scan(&eventLogId) if err != nil { diff --git a/router/common.go b/router/common.go index dc8d484..44d54a5 100644 --- a/router/common.go +++ b/router/common.go @@ -2,6 +2,9 @@ package router import ( "context" + "fmt" + "io" + "net/http" "github.com/danielgtaylor/huma/v2" "github.com/nbtca/saturday/model/dto" @@ -10,11 +13,56 @@ import ( type CommonRouter struct{} +const ( + MaxUploadSize = 10 << 20 // 10MB +) + func (CommonRouter) Upload(ctx context.Context, input *UploadFileInput) (*util.CommonResponse[dto.FileUploadResponse], error) { - // TODO: Implement multipart file upload with Huma - // For now, this needs special handling since Huma's multipart support may require custom implementation - // This endpoint may need to remain as Gin for now until Huma multipart is properly implemented - return nil, huma.Error501NotImplemented("Upload endpoint migration pending - requires multipart support") + // Get the uploaded file from the parsed multipart form + file := input.RawBody.Data().File + + // Validate file size + if file.Size > MaxUploadSize { + return nil, huma.Error400BadRequest(fmt.Sprintf("File size exceeds maximum allowed size of %d bytes", MaxUploadSize)) + } + + // Additional validation: check file signature (magic bytes) + buffer := make([]byte, 512) + n, err := file.Read(buffer) + if err != nil && err != io.EOF { + return nil, huma.Error400BadRequest("Failed to read file header: " + err.Error()) + } + + // Detect content type from file content + contentType := http.DetectContentType(buffer[:n]) + allowedTypes := []string{"image/jpeg", "image/png", "image/webp"} + isAllowed := false + for _, allowed := range allowedTypes { + if contentType == allowed { + isAllowed = true + break + } + } + if !isAllowed { + return nil, huma.Error400BadRequest(fmt.Sprintf("Invalid image file type detected: %s. Allowed types: JPEG, PNG, WebP", contentType)) + } + + // Reset file pointer after signature check + if _, err := file.Seek(0, 0); err != nil { + return nil, huma.Error500InternalServerError("Failed to reset file pointer: " + err.Error()) + } + + // Upload to Aliyun OSS + url, err := util.Upload(file.Filename, file) + if err != nil { + return nil, huma.Error500InternalServerError("Failed to upload file: " + err.Error()) + } + + return &util.CommonResponse[dto.FileUploadResponse]{ + Body: dto.FileUploadResponse{ + Url: url, + }, + }, nil } var CommonRouterApp = CommonRouter{} diff --git a/router/event.go b/router/event.go index 1328e2c..93b8f91 100644 --- a/router/event.go +++ b/router/event.go @@ -205,7 +205,11 @@ func (EventRouter) Commit(ctx context.Context, input *CommitEventInput) (*util.C if input.Body.Size != "" { event.Size = input.Body.Size } - if err := service.EventServiceApp.Act(&event, identity, util.Commit, input.Body.Content); err != nil { + opts := service.ActOptions{ + Description: input.Body.Content, + Images: input.Body.Images, + } + if err := service.EventServiceApp.ActWithOptions(&event, identity, util.Commit, opts); err != nil { return nil, huma.Error422UnprocessableEntity(err.Error()) } return util.MakeCommonResponse(event), nil @@ -226,7 +230,11 @@ func (EventRouter) AlterCommit(ctx context.Context, input *AlterCommitEventInput if input.Body.Size != "" { event.Size = input.Body.Size } - if err := service.EventServiceApp.Act(&event, identity, util.AlterCommit, input.Body.Content); err != nil { + opts := service.ActOptions{ + Description: input.Body.Content, + Images: input.Body.Images, + } + if err := service.EventServiceApp.ActWithOptions(&event, identity, util.AlterCommit, opts); err != nil { return nil, huma.Error422UnprocessableEntity(err.Error()) } return util.MakeCommonResponse(event), nil @@ -313,6 +321,7 @@ func (EventRouter) Create(ctx context.Context, input *CreateClientEventInput) (* QQ: input.Body.QQ, ContactPreference: input.Body.ContactPreference, Problem: input.Body.Problem, + Images: model.StringSlice(input.Body.Images), } err = service.EventServiceApp.CreateEvent(event) if err != nil { @@ -358,6 +367,9 @@ func (er EventRouter) Update(ctx context.Context, input *UpdateClientEventInput) if input.Body.Size != "" { event.Size = input.Body.Size } + if input.Body.Images != nil { + event.Images = model.StringSlice(input.Body.Images) + } if err := service.EventServiceApp.Act(&event, identity, util.Update); err != nil { return nil, huma.Error422UnprocessableEntity(err.Error()) } diff --git a/router/input_types.go b/router/input_types.go index bf69959..6d81cea 100644 --- a/router/input_types.go +++ b/router/input_types.go @@ -1,6 +1,7 @@ package router import ( + "github.com/danielgtaylor/huma/v2" "github.com/nbtca/saturday/model" "github.com/nbtca/saturday/model/dto" ) @@ -120,8 +121,9 @@ type CommitEventInput struct { MemberAuthInput EventPathInput Body struct { - Content string `json:"content"` - Size string `json:"size" required:"false"` + Content string `json:"content"` + Size string `json:"size" required:"false"` + Images []string `json:"images" required:"false"` } } @@ -129,8 +131,9 @@ type AlterCommitEventInput struct { MemberAuthInput EventPathInput Body struct { - Content string `json:"content"` - Size string `json:"size" required:"false"` + Content string `json:"content"` + Size string `json:"size" required:"false"` + Images []string `json:"images" required:"false"` } } @@ -169,11 +172,12 @@ type GetClientEventByPageInput struct { type CreateClientEventInput struct { ClientAuthInput Body struct { - Model string `json:"model" required:"false"` - Phone string `json:"phone"` - QQ string `json:"qq" required:"false"` - ContactPreference string `json:"contactPreference" required:"false"` - Problem string `json:"problem"` + Model string `json:"model" required:"false"` + Phone string `json:"phone"` + QQ string `json:"qq" required:"false"` + ContactPreference string `json:"contactPreference" required:"false"` + Problem string `json:"problem"` + Images []string `json:"images" required:"false"` } } @@ -181,12 +185,13 @@ type UpdateClientEventInput struct { ClientAuthInput EventPathInput Body struct { - Model string `json:"model" required:"false"` - Phone string `json:"phone"` - QQ string `json:"qq" required:"false"` - ContactPreference string `json:"contactPreference" required:"false"` - Problem string `json:"problem"` - Size string `json:"size" required:"false"` + Model string `json:"model" required:"false"` + Phone string `json:"phone"` + QQ string `json:"qq" required:"false"` + ContactPreference string `json:"contactPreference" required:"false"` + Problem string `json:"problem"` + Size string `json:"size" required:"false"` + Images []string `json:"images" required:"false"` } } @@ -205,7 +210,9 @@ type CreateTokenViaLogtoInput struct { type UploadFileInput struct { AuthenticatedInput - // File upload will be handled specially + RawBody huma.MultipartFormFiles[struct { + File huma.FormFile `form:"file" contentType:"image/jpeg,image/png,image/webp,image/jpg" required:"true" doc:"Image file to upload (max 10MB)"` + }] } // Webhook inputs (these may stay as Gin handlers) diff --git a/router/main.go b/router/main.go index 9d092a1..9ea828d 100644 --- a/router/main.go +++ b/router/main.go @@ -365,18 +365,16 @@ func SetupRouter() *chi.Mux { Tags: []string{"Event", "Admin"}, }, EventRouterApp.Close) - // TODO: Upload endpoint - needs special multipart handling - // For now, keep as commented until Huma multipart is implemented - /* - huma.Register(api, huma.Operation{ - OperationID: "upload-file", - Method: http.MethodPost, - Path: "/upload", - Summary: "Upload file", - Tags: []string{"Common", "Private"}, - Middlewares: huma.Middlewares{middleware.HumaAuth("member", "admin", "client")}, - }, CommonRouterApp.Upload) - */ + // Upload endpoint - multipart file upload for images + huma.Register(api, huma.Operation{ + OperationID: "upload-file", + Method: http.MethodPost, + Path: "/upload", + Summary: "Upload image file", + Description: "Upload an image file (JPEG, PNG, WebP). Max size: 10MB. Use 'file' as the field name in multipart form.", + Tags: []string{"Common", "Private"}, + Middlewares: huma.Middlewares{middleware.RequireAuth("member", "admin", "client")}, + }, CommonRouterApp.Upload) return router } diff --git a/service/event.go b/service/event.go index adb481f..6cd65fb 100644 --- a/service/event.go +++ b/service/event.go @@ -24,6 +24,10 @@ import ( type EventService struct{} +type ActOptions struct { + Description string + Images []string +} // ExportMetadata contains metadata information for Excel export type ExportMetadata struct { ExportTime time.Time @@ -863,14 +867,21 @@ this function validates the action and then perform action to the event. it also persists the event and event log. */ func (service EventService) Act(event *model.Event, identity model.Identity, action util.Action, description ...string) error { + opts := ActOptions{} + for _, d := range description { + opts.Description = fmt.Sprint(opts.Description, d) + } + return service.ActWithOptions(event, identity, action, opts) +} + +func (service EventService) ActWithOptions(event *model.Event, identity model.Identity, action util.Action, opts ActOptions) error { handler := util.MakeEventActionHandler(action, event, identity) if err := handler.ValidateAction(); err != nil { util.Logger.Error("validate action failed: ", err) return err } - for _, d := range description { - handler.Description = fmt.Sprint(handler.Description, d) - } + handler.Description = opts.Description + handler.Images = opts.Images log := handler.Handle() diff --git a/util/event-action.go b/util/event-action.go index 7824344..39b3cda 100644 --- a/util/event-action.go +++ b/util/event-action.go @@ -39,6 +39,7 @@ type eventActionHandler struct { prevStatus string nextStatus string Description string + Images []string customLog customLogFunc } @@ -52,6 +53,7 @@ var idAndDescriptionLog customLogFunc = func(eh *eventActionHandler) model.Event return eh.createEventLog(createEventLogArgs{ Id: eh.actor.Id, Description: eh.Description, + Images: eh.Images, }) } @@ -183,6 +185,7 @@ func (eh *eventActionHandler) ValidateAction() error { type createEventLogArgs struct { Id string Description string + Images []string } func (eh *eventActionHandler) createEventLog(args createEventLogArgs) model.EventLog { @@ -191,6 +194,7 @@ func (eh *eventActionHandler) createEventLog(args createEventLogArgs) model.Even Action: string(eh.action), MemberId: args.Id, Description: args.Description, + Images: args.Images, GmtCreate: GetDate(), } }