Files
EvoBGP/internal/httpapi/routes_api_keys.go
T
DenozordecandCursor 4d83b8d673
CI / changes (push) Successful in 6s
CI / commitlint (push) Skipped
CI / openapi (push) Successful in 27s
CI / web (push) Successful in 51s
CI / go (push) Successful in 2m19s
CI / bird2 (push) Successful in 13s
CI / release (push) Successful in 4m24s
feat(auth): integrate portal JWT for enhanced authentication and authorization
Added support for portal JWT authentication, enabling single sign-on (SSO) capabilities. Updated the application to handle JWT claims for user permissions and roles, enhancing security and access control. Refactored relevant components and API routes to accommodate the new authentication flow, ensuring a seamless user experience. Updated documentation to reflect the new authentication requirements and configurations.

Co-authored-by: Cursor <[email protected]>
2026-07-18 23:23:52 +07:00

227 lines
6.3 KiB
Go

package httpapi
import (
"encoding/json"
"net/http"
"strings"
"time"
"evobgp/internal/store"
)
func (s *Server) registerAPIKeyRoutes(m *http.ServeMux) {
m.HandleFunc("GET /auth/session", s.handleAuthSession)
m.HandleFunc("GET /api-keys", s.handleListAPIKeys)
m.HandleFunc("POST /api-keys", s.handlePostAPIKey)
m.HandleFunc("GET /api-keys/{id}", s.handleGetAPIKey)
m.HandleFunc("PATCH /api-keys/{id}", s.handlePatchAPIKey)
m.HandleFunc("DELETE /api-keys/{id}", s.handleDeleteAPIKey)
m.HandleFunc("POST /api-keys/{id}/rotate", s.handleRotateAPIKey)
}
func (s *Server) handleAuthSession(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok {
writeProblem(w, http.StatusUnauthorized, "Unauthorized", "missing auth")
return
}
resp := map[string]any{
"tenant_id": a.TenantID,
"role": a.Role,
"kind": a.Kind,
}
if a.Kind == AuthKindJWT {
resp["user_id"] = a.UserID
resp["email"] = a.Email
resp["permissions"] = a.Permissions
resp["is_admin"] = a.IsAdmin
}
writeJSON(w, http.StatusOK, resp)
}
func apiKeyJSON(k *store.APIKey) map[string]any {
m := map[string]any{
"id": k.ID,
"name": k.Name,
"role": k.Role,
"prefix": k.Prefix,
"created_at": k.CreatedAt.UTC().Format(time.RFC3339),
"updated_at": k.UpdatedAt.UTC().Format(time.RFC3339),
}
if k.ExpiresAt != nil {
m["expires_at"] = k.ExpiresAt.UTC().Format(time.RFC3339)
} else {
m["expires_at"] = nil
}
if k.RevokedAt != nil {
m["revoked_at"] = k.RevokedAt.UTC().Format(time.RFC3339)
} else {
m["revoked_at"] = nil
}
if k.LastUsedAt != nil {
m["last_used_at"] = k.LastUsedAt.UTC().Format(time.RFC3339)
} else {
m["last_used_at"] = nil
}
return m
}
func (s *Server) handleListAPIKeys(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok || !s.requirePerm(w, a, "bgp:access:admin") {
return
}
list, err := s.store.ListAPIKeys(a.TenantID)
if err != nil {
writeStoreErr(w, err)
return
}
writePaginatedListJSON(w, r, list, func(k *store.APIKey) map[string]any {
return apiKeyJSON(k)
})
}
func (s *Server) handleGetAPIKey(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok || !s.requirePerm(w, a, "bgp:access:admin") {
return
}
k, err := s.store.GetAPIKey(a.TenantID, r.PathValue("id"))
if err != nil {
writeStoreErr(w, err)
return
}
writeJSON(w, http.StatusOK, apiKeyJSON(k))
}
func (s *Server) handlePostAPIKey(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok || !s.requirePerm(w, a, "bgp:access:admin") {
return
}
var body struct {
Name string `json:"name"`
Role string `json:"role"`
ExpiresAt *string `json:"expires_at"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
return
}
in := &store.APIKeyCreate{
Name: strings.TrimSpace(body.Name),
Role: strings.TrimSpace(body.Role),
}
if body.ExpiresAt != nil && strings.TrimSpace(*body.ExpiresAt) != "" {
t, err := time.Parse(time.RFC3339, strings.TrimSpace(*body.ExpiresAt))
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "invalid expires_at")
return
}
in.ExpiresAt = &t
}
created, err := s.store.CreateAPIKey(a.TenantID, in)
if err != nil {
writeStoreErr(w, err)
return
}
if err := s.keyResolver.Reload(s.store); err != nil {
writeProblem(w, http.StatusInternalServerError, "Internal Server Error", "failed to reload api keys")
return
}
out := apiKeyJSON(&created.APIKey)
out["token"] = created.Token
writeJSON(w, http.StatusCreated, out)
}
func (s *Server) handlePatchAPIKey(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok || !s.requirePerm(w, a, "bgp:access:admin") {
return
}
var raw map[string]json.RawMessage
if err := json.NewDecoder(r.Body).Decode(&raw); err != nil {
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
return
}
patch := &store.APIKeyPatch{}
if v, ok := raw["name"]; ok {
var name string
if err := json.Unmarshal(v, &name); err != nil {
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid name")
return
}
patch.Name = &name
}
if v, ok := raw["role"]; ok {
var role string
if err := json.Unmarshal(v, &role); err != nil {
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid role")
return
}
patch.Role = &role
}
if v, ok := raw["expires_at"]; ok {
if string(v) == "null" {
patch.ClearExpiresAt = true
} else {
var s string
if err := json.Unmarshal(v, &s); err != nil {
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid expires_at")
return
}
t, err := time.Parse(time.RFC3339, strings.TrimSpace(s))
if err != nil {
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "invalid expires_at")
return
}
patch.ExpiresAt = &t
}
}
k, err := s.store.UpdateAPIKey(a.TenantID, r.PathValue("id"), patch)
if err != nil {
writeStoreErr(w, err)
return
}
if err := s.keyResolver.Reload(s.store); err != nil {
writeProblem(w, http.StatusInternalServerError, "Internal Server Error", "failed to reload api keys")
return
}
writeJSON(w, http.StatusOK, apiKeyJSON(k))
}
func (s *Server) handleDeleteAPIKey(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok || !s.requirePerm(w, a, "bgp:access:admin") {
return
}
if err := s.store.RevokeAPIKey(a.TenantID, r.PathValue("id")); err != nil {
writeStoreErr(w, err)
return
}
if err := s.keyResolver.Reload(s.store); err != nil {
writeProblem(w, http.StatusInternalServerError, "Internal Server Error", "failed to reload api keys")
return
}
w.WriteHeader(http.StatusNoContent)
}
func (s *Server) handleRotateAPIKey(w http.ResponseWriter, r *http.Request) {
a, ok := authFromContext(r.Context())
if !ok || !s.requirePerm(w, a, "bgp:access:admin") {
return
}
rotated, err := s.store.RotateAPIKey(a.TenantID, r.PathValue("id"))
if err != nil {
writeStoreErr(w, err)
return
}
if err := s.keyResolver.Reload(s.store); err != nil {
writeProblem(w, http.StatusInternalServerError, "Internal Server Error", "failed to reload api keys")
return
}
out := apiKeyJSON(&rotated.APIKey)
out["token"] = rotated.Token
writeJSON(w, http.StatusOK, out)
}