summaryrefslogtreecommitdiff
path: root/internal/api/roles.go
blob: aa5c807988aec75a10eb3a768b64ab892b411858 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package api

import (
	"encoding/json"
	"net/http"
	"strconv"
	"time"

	"github.com/thepeterstone/claudomator/internal/role"
	"github.com/thepeterstone/claudomator/internal/storage"
)

// roleVersionView is the JSON shape returned for a single role_configs
// version — decodes the stored config_json back into a role.RoleConfig so
// clients get structured fields rather than an opaque string.
type roleVersionView struct {
	ID          string          `json:"id"`
	Role        string          `json:"role"`
	Version     int             `json:"version"`
	Status      string          `json:"status"`
	Config      role.RoleConfig `json:"config"`
	CreatedAt   string          `json:"created_at"`
	ActivatedAt string          `json:"activated_at,omitempty"`
	RetiredAt   string          `json:"retired_at,omitempty"`
	ProposedBy  string          `json:"proposed_by,omitempty"`
}

// handleListRoleNames handles GET /api/roles — every distinct role name
// that has at least one role_configs row. There is no other way to discover
// which roles exist short of guessing names; the role/config management
// panel uses this as its entry point before fetching each role's version
// history via GET /api/roles/{role}/versions.
func (s *Server) handleListRoleNames(w http.ResponseWriter, _ *http.Request) {
	names, err := s.store.ListRoleNames()
	if err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
		return
	}
	if names == nil {
		names = []string{}
	}
	writeJSON(w, http.StatusOK, names)
}

// handleCreateRoleVersion handles POST /api/roles/{role}/versions. The
// request body is a role.RoleConfig (config_json shape, plus an optional
// proposed_by field); a new draft version is created with the next version
// number for that role. Mirrors handleCreateProject's shape (see
// internal/api/projects.go).
func (s *Server) handleCreateRoleVersion(w http.ResponseWriter, r *http.Request) {
	roleName := r.PathValue("role")
	if roleName == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "role is required"})
		return
	}

	var input struct {
		role.RoleConfig
		ProposedBy string `json:"proposed_by"`
	}
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()})
		return
	}
	input.RoleConfig.Role = roleName

	proposedBy := input.ProposedBy
	if proposedBy == "" {
		proposedBy = "human"
	}

	configJSON, err := json.Marshal(input.RoleConfig)
	if err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
		return
	}

	created, err := s.store.CreateRoleConfig(roleName, string(configJSON), proposedBy)
	if err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
		return
	}
	writeJSON(w, http.StatusCreated, toRoleVersionView(created))
}

// handleListRoleVersions handles GET /api/roles/{role}/versions.
func (s *Server) handleListRoleVersions(w http.ResponseWriter, r *http.Request) {
	roleName := r.PathValue("role")
	rows, err := s.store.ListRoleConfigVersions(roleName)
	if err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
		return
	}
	views := make([]*roleVersionView, 0, len(rows))
	for _, row := range rows {
		views = append(views, toRoleVersionView(row))
	}
	writeJSON(w, http.StatusOK, views)
}

// handleActivateRoleVersion handles POST /api/roles/{role}/activate?version=N.
func (s *Server) handleActivateRoleVersion(w http.ResponseWriter, r *http.Request) {
	roleName := r.PathValue("role")
	versionStr := r.URL.Query().Get("version")
	version, err := strconv.Atoi(versionStr)
	if err != nil || version < 1 {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid or missing version query param"})
		return
	}
	if err := s.store.ActivateRoleConfigVersion(roleName, version); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	writeJSON(w, http.StatusOK, map[string]interface{}{
		"message": "role config version activated",
		"role":    roleName,
		"version": version,
	})
}

func toRoleVersionView(row *storage.RoleConfigRow) *roleVersionView {
	v := &roleVersionView{
		ID:         row.ID,
		Role:       row.Role,
		Version:    row.Version,
		Status:     row.Status,
		CreatedAt:  row.CreatedAt.Format(time.RFC3339),
		ProposedBy: row.ProposedBy,
	}
	if row.ActivatedAt != nil {
		v.ActivatedAt = row.ActivatedAt.Format(time.RFC3339)
	}
	if row.RetiredAt != nil {
		v.RetiredAt = row.RetiredAt.Format(time.RFC3339)
	}
	_ = json.Unmarshal([]byte(row.ConfigJSON), &v.Config)
	return v
}