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
- `POST /api/projects/{id}/archive` archives a project of the caller's organisation and answers `204 No Content`. An archived project drops out of the organisation's project list (the list endpoint already hides projects whose `archived_at` is set) and stays readable through `GET /api/projects/{id}`. Unarchiving is a separate PR.
- Only the project's owner or an admin of its organisation may archive it. Any other signed-in member of the organisation gets `403 forbidden`, also when the project is already archived, and the project is left as it was.
- As in `GET /api/projects/{id}`: an id that is not a positive integer, an unknown project, or a project of another organisation (even for an admin of that other organisation) answers `404 project not found`, and a store error is logged and answers `500 internal error`. Archiving a project that is already archived answers `409`, and its original `archived_at` is kept.
Files touched
- internal/projects/handler.go
- cmd/server/main.go
--- 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)