feat: complete MVP epics E7-E10 (provisioning, console proxy, audit, frontend)
This commit is contained in:
@@ -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})
|
||||
}
|
||||
Reference in New Issue
Block a user