feat: complete MVP epics E7-E10 (provisioning, console proxy, audit, frontend)

This commit is contained in:
Philipp
2026-06-12 10:45:13 +02:00
parent 137c13fa98
commit ac30a19960
33 changed files with 3625 additions and 263 deletions
+205
View File
@@ -0,0 +1,205 @@
package sshkey
import (
"context"
"encoding/json"
"net/http"
"strings"
"time"
"proxui/backend/internal/auth"
"proxui/backend/internal/membership"
"proxui/backend/internal/rbac"
)
type Repository interface {
List(ctx context.Context, profileID string, tenantID string) ([]SSHKey, bool, error)
Get(ctx context.Context, profileID string, keyID string) (SSHKey, bool, error)
Create(ctx context.Context, profileID string, tenantID string, name string, publicKey string) (SSHKey, bool, error)
Delete(ctx context.Context, profileID string, keyID string) (bool, bool, error)
}
type Handler struct {
repository Repository
}
func NewHandler(repository Repository) Handler {
return Handler{repository: repository}
}
func (h Handler) ListTenantKeys(w http.ResponseWriter, r *http.Request) {
principal, ok := auth.PrincipalFromRequest(r)
if !ok {
writeError(w, http.StatusUnauthorized, "unauthorized")
return
}
tenantID := r.PathValue("tenantID")
if tenantID == "" {
writeError(w, http.StatusBadRequest, "tenant_id_required")
return
}
keys, found, err := h.repository.List(r.Context(), principal.Subject, tenantID)
if err != nil {
writeError(w, http.StatusInternalServerError, "ssh_keys_list_failed")
return
}
if !found {
writeError(w, http.StatusNotFound, "tenant_not_found")
return
}
writeJSON(w, http.StatusOK, map[string][]SSHKey{"data": keys})
}
func (h Handler) GetKey(w http.ResponseWriter, r *http.Request) {
principal, ok := auth.PrincipalFromRequest(r)
if !ok {
writeError(w, http.StatusUnauthorized, "unauthorized")
return
}
keyID := r.PathValue("keyID")
if keyID == "" {
writeError(w, http.StatusBadRequest, "key_id_required")
return
}
key, found, err := h.repository.Get(r.Context(), principal.Subject, keyID)
if err != nil {
writeError(w, http.StatusInternalServerError, "ssh_key_get_failed")
return
}
if !found {
writeError(w, http.StatusNotFound, "ssh_key_not_found")
return
}
membershipID, _ := membership.FromRequest(r)
if membershipID.TenantID != key.TenantID {
writeError(w, http.StatusForbidden, "forbidden")
return
}
if !rbac.Can(rbac.Role(membershipID.Role), rbac.ActionSSHKeyRead) {
writeError(w, http.StatusForbidden, "forbidden")
return
}
writeJSON(w, http.StatusOK, key)
}
func (h Handler) CreateKey(w http.ResponseWriter, r *http.Request) {
principal, ok := auth.PrincipalFromRequest(r)
if !ok {
writeError(w, http.StatusUnauthorized, "unauthorized")
return
}
tenantID := r.PathValue("tenantID")
if tenantID == "" {
writeError(w, http.StatusBadRequest, "tenant_id_required")
return
}
membershipID, _ := membership.FromRequest(r)
if membershipID.TenantID != tenantID {
writeError(w, http.StatusForbidden, "forbidden")
return
}
if !rbac.Can(rbac.Role(membershipID.Role), rbac.ActionSSHKeyManage) {
writeError(w, http.StatusForbidden, "forbidden")
return
}
var req createRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid_body")
return
}
req.Name = strings.TrimSpace(req.Name)
if req.Name == "" {
writeError(w, http.StatusBadRequest, "name_required")
return
}
req.PublicKey = strings.TrimSpace(req.PublicKey)
if req.PublicKey == "" {
writeError(w, http.StatusBadRequest, "public_key_required")
return
}
key, found, err := h.repository.Create(r.Context(), principal.Subject, tenantID, req.Name, req.PublicKey)
if err != nil {
writeError(w, http.StatusInternalServerError, "ssh_key_create_failed")
return
}
if !found {
writeError(w, http.StatusNotFound, "tenant_not_found")
return
}
writeJSON(w, http.StatusCreated, key)
}
func (h Handler) DeleteKey(w http.ResponseWriter, r *http.Request) {
principal, ok := auth.PrincipalFromRequest(r)
if !ok {
writeError(w, http.StatusUnauthorized, "unauthorized")
return
}
keyID := r.PathValue("keyID")
if keyID == "" {
writeError(w, http.StatusBadRequest, "key_id_required")
return
}
deleted, member, err := h.repository.Delete(r.Context(), principal.Subject, keyID)
if err != nil {
writeError(w, http.StatusInternalServerError, "ssh_key_delete_failed")
return
}
if !member {
writeError(w, http.StatusNotFound, "ssh_key_not_found")
return
}
if !deleted {
writeError(w, http.StatusNotFound, "ssh_key_not_found")
return
}
membershipID, _ := membership.FromRequest(r)
if !rbac.Can(rbac.Role(membershipID.Role), rbac.ActionSSHKeyManage) {
writeError(w, http.StatusForbidden, "forbidden")
return
}
w.WriteHeader(http.StatusNoContent)
}
type SSHKey struct {
ID string `json:"id"`
TenantID string `json:"tenant_id"`
Name string `json:"name"`
PublicKey string `json:"public_key"`
CreatedAt time.Time `json:"created_at"`
}
type createRequest struct {
Name string `json:"name"`
PublicKey string `json:"public_key"`
}
func writeJSON(w http.ResponseWriter, status int, body any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(body)
}
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]string{"error": message})
}