package handlers import ( "crypto/rand" "database/sql" "encoding/hex" "encoding/json" "fmt" "net/http" "strconv" "strings" "time" "golang.org/x/crypto/bcrypt" ) // UsersHandler hanterar alla /api/v1/users/* rutter. type UsersHandler struct { DB *sql.DB } // EnsureSchema lägger till deleted_at om kolumnen saknas. func (h *UsersHandler) EnsureSchema() error { _, err := h.DB.Exec( `ALTER TABLE users ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ`, ) return err } // adminUser är den fullständiga representationen som returneras av admin-API:et. type adminUser struct { ID string `json:"id"` Email string `json:"email"` Name string `json:"name"` Role string `json:"role"` CreatedAt time.Time `json:"created_at"` LastLogin time.Time `json:"last_login"` } type createUserReq struct { Email string `json:"email"` Name string `json:"name"` Role string `json:"role"` Password string `json:"password"` } type updateUserReq struct { Name string `json:"name"` Role string `json:"role"` } type usersListResp struct { Users []adminUser `json:"users"` Total int `json:"total"` Page int `json:"page"` Limit int `json:"limit"` } // ServeHTTP dispatchar till list/create/get/update/softDelete baserat på metod och path. func (h *UsersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { tail := strings.TrimPrefix(r.URL.Path, "/api/v1/users") if tail == "" || tail == "/" { switch r.Method { case http.MethodGet: h.list(w, r) case http.MethodPost: h.create(w, r) default: writeError(w, http.StatusMethodNotAllowed, "method not allowed") } return } id := strings.TrimPrefix(tail, "/") if id == "" || strings.Contains(id, "/") { writeError(w, http.StatusNotFound, "not found") return } switch r.Method { case http.MethodGet: h.get(w, r, id) case http.MethodPut: h.update(w, r, id) case http.MethodDelete: h.softDelete(w, r, id) default: writeError(w, http.StatusMethodNotAllowed, "method not allowed") } } // GET /api/v1/users?page=1&limit=20 func (h *UsersHandler) list(w http.ResponseWriter, r *http.Request) { page, limit := parsePagination(r) offset := (page - 1) * limit var total int if err := h.DB.QueryRowContext(r.Context(), `SELECT COUNT(*) FROM users WHERE deleted_at IS NULL`, ).Scan(&total); err != nil { writeError(w, http.StatusInternalServerError, "db error") return } rows, err := h.DB.QueryContext(r.Context(), `SELECT id, email, name, role, created_at, last_login FROM users WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT $1 OFFSET $2`, limit, offset, ) if err != nil { writeError(w, http.StatusInternalServerError, "db error") return } defer rows.Close() users := make([]adminUser, 0, limit) for rows.Next() { var u adminUser if err := rows.Scan(&u.ID, &u.Email, &u.Name, &u.Role, &u.CreatedAt, &u.LastLogin); err != nil { writeError(w, http.StatusInternalServerError, "scan error") return } users = append(users, u) } if err := rows.Err(); err != nil { writeError(w, http.StatusInternalServerError, "rows error") return } writeJSON(w, http.StatusOK, usersListResp{ Users: users, Total: total, Page: page, Limit: limit, }) } // GET /api/v1/users/{id} func (h *UsersHandler) get(w http.ResponseWriter, r *http.Request, id string) { var u adminUser err := h.DB.QueryRowContext(r.Context(), `SELECT id, email, name, role, created_at, last_login FROM users WHERE id = $1 AND deleted_at IS NULL`, id, ).Scan(&u.ID, &u.Email, &u.Name, &u.Role, &u.CreatedAt, &u.LastLogin) if err == sql.ErrNoRows { writeError(w, http.StatusNotFound, "user not found") return } if err != nil { writeError(w, http.StatusInternalServerError, "db error") return } writeJSON(w, http.StatusOK, u) } // POST /api/v1/users func (h *UsersHandler) create(w http.ResponseWriter, r *http.Request) { var req createUserReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid json") return } req.Email = strings.TrimSpace(strings.ToLower(req.Email)) if req.Email == "" { writeError(w, http.StatusBadRequest, "email required") return } if req.Password == "" { writeError(w, http.StatusBadRequest, "password required") return } if req.Role == "" { req.Role = "user" } if req.Role != "admin" && req.Role != "user" { writeError(w, http.StatusBadRequest, "role must be admin or user") return } hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { writeError(w, http.StatusInternalServerError, "password hash failed") return } id, err := newUUID() if err != nil { writeError(w, http.StatusInternalServerError, "id generation failed") return } now := time.Now().UTC() _, err = h.DB.ExecContext(r.Context(), `INSERT INTO users (id, email, name, role, password_hash, created_at, last_login) VALUES ($1, $2, $3, $4, $5, $6, $7)`, id, req.Email, req.Name, req.Role, string(hash), now, now, ) if err != nil { if strings.Contains(err.Error(), "unique") || strings.Contains(err.Error(), "duplicate") { writeError(w, http.StatusConflict, "email already exists") return } writeError(w, http.StatusInternalServerError, "db error") return } writeJSON(w, http.StatusCreated, adminUser{ ID: id, Email: req.Email, Name: req.Name, Role: req.Role, CreatedAt: now, LastLogin: now, }) } // PUT /api/v1/users/{id} func (h *UsersHandler) update(w http.ResponseWriter, r *http.Request, id string) { var req updateUserReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid json") return } if req.Role != "" && req.Role != "admin" && req.Role != "user" { writeError(w, http.StatusBadRequest, "role must be admin or user") return } // NULLIF gör att tom role-sträng bevarar befintligt värde. res, err := h.DB.ExecContext(r.Context(), `UPDATE users SET name = $1, role = COALESCE(NULLIF($2, ''), role) WHERE id = $3 AND deleted_at IS NULL`, req.Name, req.Role, id, ) if err != nil { writeError(w, http.StatusInternalServerError, "db error") return } if n, _ := res.RowsAffected(); n == 0 { writeError(w, http.StatusNotFound, "user not found") return } h.get(w, r, id) } // DELETE /api/v1/users/{id} — soft delete, sätter deleted_at func (h *UsersHandler) softDelete(w http.ResponseWriter, r *http.Request, id string) { res, err := h.DB.ExecContext(r.Context(), `UPDATE users SET deleted_at = NOW() WHERE id = $1 AND deleted_at IS NULL`, id, ) if err != nil { writeError(w, http.StatusInternalServerError, "db error") return } if n, _ := res.RowsAffected(); n == 0 { writeError(w, http.StatusNotFound, "user not found") return } w.WriteHeader(http.StatusNoContent) } func parsePagination(r *http.Request) (page, limit int) { page, limit = 1, 20 if v, err := strconv.Atoi(r.URL.Query().Get("page")); err == nil && v > 0 { page = v } if v, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && v > 0 && v <= 100 { limit = v } return } // newUUID genererar ett random UUID v4. func newUUID() (string, error) { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { return "", err } b[6] = (b[6] & 0x0f) | 0x40 // version 4 b[8] = (b[8] & 0x3f) | 0x80 // variant RFC 4122 return fmt.Sprintf("%s-%s-%s-%s-%s", hex.EncodeToString(b[0:4]), hex.EncodeToString(b[4:6]), hex.EncodeToString(b[6:8]), hex.EncodeToString(b[8:10]), hex.EncodeToString(b[10:]), ), nil }