package admin import ( "context" "net/http" "strconv" "strings" "time" "github.com/Wei-Shaw/sub2api/internal/pkg/response" "github.com/Wei-Shaw/sub2api/internal/pkg/sysutil" middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/gin-gonic/gin" ) // SystemHandler handles system-related operations type SystemHandler struct { updateSvc *service.UpdateService lockSvc *service.SystemOperationLockService } // NewSystemHandler creates a new SystemHandler func NewSystemHandler(updateSvc *service.UpdateService, lockSvc *service.SystemOperationLockService) *SystemHandler { return &SystemHandler{ updateSvc: updateSvc, lockSvc: lockSvc, } } // GetVersion returns the current version // GET /api/v1/admin/system/version func (h *SystemHandler) GetVersion(c *gin.Context) { info, _ := h.updateSvc.CheckUpdate(c.Request.Context(), false) response.Success(c, gin.H{ "version": info.CurrentVersion, }) } // CheckUpdates checks for available updates // GET /api/v1/admin/system/check-updates func (h *SystemHandler) CheckUpdates(c *gin.Context) { force := c.Query("force") == "true" info, err := h.updateSvc.CheckUpdate(c.Request.Context(), force) if err != nil { response.Error(c, http.StatusInternalServerError, err.Error()) return } response.Success(c, info) } // PerformUpdate downloads and applies the update // POST /api/v1/admin/system/update func (h *SystemHandler) PerformUpdate(c *gin.Context) { operationID := buildSystemOperationID(c, "update") payload := gin.H{"operation_id": operationID} executeAdminIdempotentJSON(c, "admin.system.update", payload, service.DefaultSystemOperationIdempotencyTTL(), func(ctx context.Context) (any, error) { lock, release, err := h.acquireSystemLock(ctx, operationID) if err != nil { return nil, err } var releaseReason string succeeded := false defer func() { release(releaseReason, succeeded) }() if err := h.updateSvc.PerformUpdate(ctx); err != nil { releaseReason = "SYSTEM_UPDATE_FAILED" return nil, err } succeeded = true return gin.H{ "message": "Update completed. Please restart the service.", "need_restart": true, "operation_id": lock.OperationID(), }, nil }) } // Rollback restores the previous version // POST /api/v1/admin/system/rollback func (h *SystemHandler) Rollback(c *gin.Context) { operationID := buildSystemOperationID(c, "rollback") payload := gin.H{"operation_id": operationID} executeAdminIdempotentJSON(c, "admin.system.rollback", payload, service.DefaultSystemOperationIdempotencyTTL(), func(ctx context.Context) (any, error) { lock, release, err := h.acquireSystemLock(ctx, operationID) if err != nil { return nil, err } var releaseReason string succeeded := false defer func() { release(releaseReason, succeeded) }() if err := h.updateSvc.Rollback(); err != nil { releaseReason = "SYSTEM_ROLLBACK_FAILED" return nil, err } succeeded = true return gin.H{ "message": "Rollback completed. Please restart the service.", "need_restart": true, "operation_id": lock.OperationID(), }, nil }) } // RestartService restarts the systemd service // POST /api/v1/admin/system/restart func (h *SystemHandler) RestartService(c *gin.Context) { operationID := buildSystemOperationID(c, "restart") payload := gin.H{"operation_id": operationID} executeAdminIdempotentJSON(c, "admin.system.restart", payload, service.DefaultSystemOperationIdempotencyTTL(), func(ctx context.Context) (any, error) { lock, release, err := h.acquireSystemLock(ctx, operationID) if err != nil { return nil, err } succeeded := false defer func() { release("", succeeded) }() // Schedule service restart in background after sending response // This ensures the client receives the success response before the service restarts go func() { // Wait a moment to ensure the response is sent time.Sleep(500 * time.Millisecond) sysutil.RestartServiceAsync() }() succeeded = true return gin.H{ "message": "Service restart initiated", "operation_id": lock.OperationID(), }, nil }) } func (h *SystemHandler) acquireSystemLock( ctx context.Context, operationID string, ) (*service.SystemOperationLock, func(string, bool), error) { if h.lockSvc == nil { return nil, nil, service.ErrIdempotencyStoreUnavail } lock, err := h.lockSvc.Acquire(ctx, operationID) if err != nil { return nil, nil, err } release := func(reason string, succeeded bool) { releaseCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() _ = h.lockSvc.Release(releaseCtx, lock, succeeded, reason) } return lock, release, nil } func buildSystemOperationID(c *gin.Context, operation string) string { key := strings.TrimSpace(c.GetHeader("Idempotency-Key")) if key == "" { return "sysop-" + operation + "-" + strconv.FormatInt(time.Now().UnixNano(), 36) } actorScope := "admin:0" if subject, ok := middleware2.GetAuthSubjectFromContext(c); ok { actorScope = "admin:" + strconv.FormatInt(subject.UserID, 10) } seed := operation + "|" + actorScope + "|" + c.FullPath() + "|" + key hash := service.HashIdempotencyKey(seed) if len(hash) > 24 { hash = hash[:24] } return "sysop-" + hash }