Let owners archive a project

Go · Go · beginner · modification

Adds an Archive action for projects so teams can clear finished work out of their project list without deleting it. The project's owner and the organisation's admins can archive; anyone else gets a 403. The handler reuses `load`, so ids, unknown projects and other organisations' projects get the same 404 as `GET`. When the owner or an admin archives a project that is already archived, the store's conditional update makes it a 409 instead of moving `archived_at`.

Go 1.22 service, standard library only (`net/http` method-pattern `ServeMux`, `log/slog`). `auth.RequireUser` (existing) checks the session cookie and, on unsafe methods such as `POST`, the CSRF token, answers 401 or 403 itself, and puts the signed-in `auth.User` (`ID` and `OrgID int64`, `Role string`: `auth.RoleAdmin` or `auth.RoleMember`) into the request context; `auth.UserFrom` reads it back. `Project` (existing) has `ID`, `OrgID`, `OwnerID`, `Name` and `ArchivedAt *time.Time`. `Store` (existing) has `Get(ctx, id)`, which returns `ErrNotFound` for an unknown id, and `Archive(ctx, id)`, added with the `archived_at` column in the previous PR: it runs `UPDATE projects SET archived_at = now() WHERE id = $1 AND archived_at IS NULL` and returns `ErrAlreadyArchived` when no row changed. Projects are never deleted, only archived, and a project never changes organisation. `Handler` (existing) holds `Store Store` and `Log *slog.Logger`; `Get` and `load` are unchanged.

Requirements

Files touched

--- internal/projects/handler.go
 }
 
+// Archive hides a project from the organisation's project list. Only the
+// project's owner or an admin of its organisation may archive it.
+func (h *Handler) Archive(w http.ResponseWriter, r *http.Request) {
+	user, ok := auth.UserFrom(r.Context())
+	if !ok {
+		http.Error(w, "unauthorized", http.StatusUnauthorized)
+		return
+	}
+	p, ok := h.load(w, r, user)
+	if !ok {
+		return
+	}
+	if p.OwnerID != user.ID && user.Role != auth.RoleAdmin {
+		http.Error(w, "forbidden", http.StatusForbidden)
+	}
+
+	err := h.Store.Archive(r.Context(), p.ID)
+	if errors.Is(err, ErrAlreadyArchived) {
+		http.Error(w, "project is already archived", http.StatusConflict)
+		return
+	}
+	if err != nil {
+		h.Log.Error("archive project", "project_id", p.ID, "err", err)
+		http.Error(w, "internal error", http.StatusInternalServerError)
+		return
+	}
+	h.Log.Info("project archived", "project_id", p.ID, "user_id", user.ID)

Review this PR

Go practice