mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
Complete MPC wallet management platform rivaling Fireblocks/Utila/Fordefi: Backend (Go): - REST API with 60+ endpoints on chi router (pkg/api/) - PostgreSQL schema with 13 tables (pkg/db/) - JWT + MFA auth, bcrypt passwords, API key management - Policy engine (deny/approve/require_approval) - Transaction signing flow with approval workflow - Subscription scheduler for recurring payments - Payment requests with public pay pages - Safe + ERC-4337 smart wallet integration (pkg/smart/) - Rate limiting, webhook delivery (HMAC-SHA256), audit logging - 27 passing tests (auth, policy engine, middleware) Frontend (Next.js 14): - 27 routes with monochrome dark theme (Tailwind + Geist fonts) - Full API client with JWT auto-refresh - React Query hooks for all resources - Pages: dashboard, vaults, wallets, transactions, policies, whitelist, team, settings, API keys, webhooks, audit, subscriptions, payments, smart wallets Infrastructure: - Dockerfiles for API (Go) and dashboard (Next.js standalone) - compose.yml for local development (PostgreSQL + API + dashboard) - K8s manifests for mpc.lux.network + cloud.lux.network - GitHub Actions deploy workflow (build → push GHCR → kubectl apply) - TLS via cert-manager, Traefik IngressRoute for both domains
135 lines
3.2 KiB
Go
135 lines
3.2 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
type contextKey string
|
|
|
|
const (
|
|
ctxUserID contextKey = "user_id"
|
|
ctxOrgID contextKey = "org_id"
|
|
ctxRole contextKey = "role"
|
|
)
|
|
|
|
func getUserID(ctx context.Context) string {
|
|
v, _ := ctx.Value(ctxUserID).(string)
|
|
return v
|
|
}
|
|
|
|
func getOrgID(ctx context.Context) string {
|
|
v, _ := ctx.Value(ctxOrgID).(string)
|
|
return v
|
|
}
|
|
|
|
func getRole(ctx context.Context) string {
|
|
v, _ := ctx.Value(ctxRole).(string)
|
|
return v
|
|
}
|
|
|
|
func urlParam(r *http.Request, key string) string {
|
|
return chi.URLParam(r, key)
|
|
}
|
|
|
|
func (s *Server) authMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Try JWT from Authorization header
|
|
auth := r.Header.Get("Authorization")
|
|
if strings.HasPrefix(auth, "Bearer ") {
|
|
token := strings.TrimPrefix(auth, "Bearer ")
|
|
claims, err := s.validateJWT(token)
|
|
if err != nil {
|
|
writeError(w, http.StatusUnauthorized, "invalid token")
|
|
return
|
|
}
|
|
ctx := context.WithValue(r.Context(), ctxUserID, claims.UserID)
|
|
ctx = context.WithValue(ctx, ctxOrgID, claims.OrgID)
|
|
ctx = context.WithValue(ctx, ctxRole, claims.Role)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
return
|
|
}
|
|
|
|
// Try API key from X-API-Key header
|
|
apiKey := r.Header.Get("X-API-Key")
|
|
if apiKey != "" {
|
|
orgID, err := s.validateAPIKey(r.Context(), apiKey)
|
|
if err != nil {
|
|
writeError(w, http.StatusUnauthorized, "invalid api key")
|
|
return
|
|
}
|
|
ctx := context.WithValue(r.Context(), ctxOrgID, orgID)
|
|
ctx = context.WithValue(ctx, ctxRole, "api")
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
return
|
|
}
|
|
|
|
writeError(w, http.StatusUnauthorized, "missing authorization")
|
|
})
|
|
}
|
|
|
|
func (s *Server) auditMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
next.ServeHTTP(w, r)
|
|
// Fire-and-forget audit log for mutating operations
|
|
if r.Method != http.MethodGet {
|
|
go s.writeAuditLog(r.Context(), r)
|
|
}
|
|
})
|
|
}
|
|
|
|
func requireRole(roles ...string) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
role := getRole(r.Context())
|
|
for _, allowed := range roles {
|
|
if role == allowed {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
}
|
|
writeError(w, http.StatusForbidden, "insufficient permissions")
|
|
})
|
|
}
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, code int, v interface{}) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(code)
|
|
json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
func writeError(w http.ResponseWriter, code int, msg string) {
|
|
writeJSON(w, code, map[string]string{"error": msg})
|
|
}
|
|
|
|
func (s *Server) writeAuditLog(ctx context.Context, r *http.Request) {
|
|
orgID := getOrgID(ctx)
|
|
userID := getUserID(ctx)
|
|
if orgID == "" {
|
|
return
|
|
}
|
|
|
|
action := r.Method + " " + r.URL.Path
|
|
ip := r.RemoteAddr
|
|
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
|
ip = strings.Split(xff, ",")[0]
|
|
}
|
|
|
|
_, _ = s.db.Pool.Exec(context.Background(),
|
|
`INSERT INTO audit_log (org_id, user_id, action, ip_address) VALUES ($1, $2, $3, $4)`,
|
|
orgID, nilIfEmpty(userID), action, ip,
|
|
)
|
|
}
|
|
|
|
func nilIfEmpty(s string) *string {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
return &s
|
|
}
|