Setup completion was previously tracked by a manually-flipped setup_complete row in system_settings, written via a JWT-protected PUT /api/setup/complete endpoint. This meant any admin user created outside the setup wizard (future CLI, seed scripts, direct DB inserts) would not flip the switch, leaving the app stuck redirecting to /setup.
The trigger is now derived from real data: setup is complete iff at least one admin user exists. This is self-correcting regardless of how users are created, and re-engages setup automatically if all admins are ever removed.
Changes:
- Add internal/setupstatus package with IsSetupComplete() (queries CountAdmins, 10s in-memory cache, fails open on DB error) and Invalidate() to clear the cache. Uses an AdminCounter interface to avoid importing the database package.
- Add CountAdmins sqlc query (SELECT COUNT(*) FROM users WHERE role = 'admin') and regenerate.
- Rewire router/setup.go isSetupComplete() to delegate to setupstatus; drop the old setup_complete setting read, cache vars, and the PUT /api/setup/complete route.
- Call setupstatus.Invalidate() in the auth handler after CreateUser, UpdateUserRole, and DeleteUser so the cache reflects admin-count changes immediately.
- Align first-user promotion in Register to key off !adminExists instead of len(users) == 0, so the two checks cannot diverge.
- Remove the now-dead SetSetupComplete/GetSetupStatus handlers.
- Drop the setup_complete seed row from schema.sql.
- Remove the apiPut('/setup/complete') call from the setup wizard finishSetup(); the admin account created in submitAdmin already marks setup complete server-side.
136 lines
4.4 KiB
Go
136 lines
4.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bookhoard/internal/database"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/labstack/echo/v5"
|
|
)
|
|
|
|
type SystemSettingsHandler struct {
|
|
db *database.Queries
|
|
}
|
|
|
|
func NewSystemSettingsHandler(db *database.Queries) *SystemSettingsHandler {
|
|
return &SystemSettingsHandler{
|
|
db: db,
|
|
}
|
|
}
|
|
|
|
type UpdateScanSettingsRequest struct {
|
|
ScanPollIntervalSeconds int32 `json:"scan_poll_interval_seconds" validate:"required,min=1,max=3600"`
|
|
AutoScanEnabled bool `json:"auto_scan_enabled"`
|
|
}
|
|
|
|
type ScanSettingsResponse struct {
|
|
ScanPollIntervalSeconds int32 `json:"scan_poll_interval_seconds"`
|
|
AutoScanEnabled bool `json:"auto_scan_enabled"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
type UpdateTimezoneSettingsRequest struct {
|
|
DefaultTimezone string `json:"default_timezone" validate:"required"`
|
|
}
|
|
|
|
func (h *SystemSettingsHandler) UpdateTimezoneSettings(c *echo.Context) error {
|
|
var req UpdateTimezoneSettingsRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"})
|
|
}
|
|
if _, err := time.LoadLocation(req.DefaultTimezone); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid timezone"})
|
|
}
|
|
err := h.db.UpdateSystemSetting(c.Request().Context(), database.UpdateSystemSettingParams{
|
|
SettingKey: "default_timezone",
|
|
SettingValue: req.DefaultTimezone,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.JSON(http.StatusOK, map[string]string{"message": "Timezone updated"})
|
|
}
|
|
|
|
func (h *SystemSettingsHandler) UpdateScanSettings(c *echo.Context) error {
|
|
var req UpdateScanSettingsRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
|
}
|
|
if err := c.Validate(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
scanFrequencyValue := strconv.FormatInt(int64(req.ScanPollIntervalSeconds), 10)
|
|
autoScanValue := strconv.FormatBool(req.AutoScanEnabled)
|
|
|
|
err := h.db.UpdateSystemSetting(c.Request().Context(), database.UpdateSystemSettingParams{
|
|
SettingKey: "scan_poll_interval_seconds",
|
|
SettingValue: scanFrequencyValue,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "system setting not found"})
|
|
}
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
err = h.db.UpdateSystemSetting(c.Request().Context(), database.UpdateSystemSettingParams{
|
|
SettingKey: "auto_scan_enabled",
|
|
SettingValue: autoScanValue,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "system setting not found"})
|
|
}
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, ScanSettingsResponse{
|
|
ScanPollIntervalSeconds: req.ScanPollIntervalSeconds,
|
|
AutoScanEnabled: req.AutoScanEnabled,
|
|
Message: "scan settings updated successfully",
|
|
})
|
|
}
|
|
|
|
func (h *SystemSettingsHandler) GetScanSettings(c *echo.Context) error {
|
|
scanFrequencySetting, err := h.db.GetSystemSetting(c.Request().Context(), "scan_poll_interval_seconds")
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return c.JSON(http.StatusOK, ScanSettingsResponse{
|
|
ScanPollIntervalSeconds: 1800,
|
|
AutoScanEnabled: true,
|
|
})
|
|
}
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
autoScanSetting, err := h.db.GetSystemSetting(c.Request().Context(), "auto_scan_enabled")
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return c.JSON(http.StatusOK, ScanSettingsResponse{
|
|
ScanPollIntervalSeconds: 1800,
|
|
AutoScanEnabled: true,
|
|
})
|
|
}
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
scanFrequency, err := strconv.ParseInt(scanFrequencySetting, 10, 32)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "invalid scan frequency setting"})
|
|
}
|
|
|
|
autoScanEnabled, err := strconv.ParseBool(autoScanSetting)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "invalid auto scan setting"})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, ScanSettingsResponse{
|
|
ScanPollIntervalSeconds: int32(scanFrequency),
|
|
AutoScanEnabled: autoScanEnabled,
|
|
})
|
|
}
|