Add full monorepo: virtual-banker, backend, frontend, docs, scripts, deployment
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
35
backend/api/realtime.go
Normal file
35
backend/api/realtime.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// HandleRealtimeWebSocket handles WebSocket upgrade for realtime communication
|
||||
func (s *Server) HandleRealtimeWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
sessionID := vars["id"]
|
||||
|
||||
if sessionID == "" {
|
||||
writeError(w, http.StatusBadRequest, "session_id is required", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Get session to validate
|
||||
_, err := s.sessionManager.GetSession(r.Context(), sessionID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusUnauthorized, "invalid session", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Upgrade to WebSocket
|
||||
if s.realtimeGateway != nil {
|
||||
if err := s.realtimeGateway.HandleWebSocket(w, r, sessionID); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to upgrade connection", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusServiceUnavailable, "realtime gateway not available", nil)
|
||||
}
|
||||
185
backend/api/routes.go
Normal file
185
backend/api/routes.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/explorer/virtual-banker/backend/realtime"
|
||||
"github.com/explorer/virtual-banker/backend/session"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Server handles HTTP requests
|
||||
type Server struct {
|
||||
sessionManager *session.Manager
|
||||
realtimeGateway *realtime.Gateway
|
||||
router *mux.Router
|
||||
}
|
||||
|
||||
// NewServer creates a new API server
|
||||
func NewServer(sessionManager *session.Manager, realtimeGateway *realtime.Gateway) *Server {
|
||||
s := &Server{
|
||||
sessionManager: sessionManager,
|
||||
realtimeGateway: realtimeGateway,
|
||||
router: mux.NewRouter(),
|
||||
}
|
||||
s.setupRoutes()
|
||||
return s
|
||||
}
|
||||
|
||||
// setupRoutes sets up all API routes
|
||||
func (s *Server) setupRoutes() {
|
||||
api := s.router.PathPrefix("/v1").Subrouter()
|
||||
|
||||
// Session routes
|
||||
api.HandleFunc("/sessions", s.handleCreateSession).Methods("POST")
|
||||
api.HandleFunc("/sessions/{id}/refresh-token", s.handleRefreshToken).Methods("POST")
|
||||
api.HandleFunc("/sessions/{id}/end", s.handleEndSession).Methods("POST")
|
||||
|
||||
// Realtime WebSocket
|
||||
api.HandleFunc("/realtime/{id}", s.HandleRealtimeWebSocket)
|
||||
|
||||
// Health check
|
||||
s.router.HandleFunc("/health", s.handleHealth).Methods("GET")
|
||||
}
|
||||
|
||||
// CreateSessionRequest represents a session creation request
|
||||
type CreateSessionRequest struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
UserID string `json:"user_id"`
|
||||
AuthAssertion string `json:"auth_assertion"`
|
||||
PortalContext map[string]interface{} `json:"portal_context,omitempty"`
|
||||
}
|
||||
|
||||
// CreateSessionResponse represents a session creation response
|
||||
type CreateSessionResponse struct {
|
||||
SessionID string `json:"session_id"`
|
||||
EphemeralToken string `json:"ephemeral_token"`
|
||||
Config *session.TenantConfig `json:"config"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
// handleCreateSession handles POST /v1/sessions
|
||||
func (s *Server) handleCreateSession(w http.ResponseWriter, r *http.Request) {
|
||||
var req CreateSessionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body", err)
|
||||
return
|
||||
}
|
||||
|
||||
if req.TenantID == "" || req.UserID == "" || req.AuthAssertion == "" {
|
||||
writeError(w, http.StatusBadRequest, "tenant_id, user_id, and auth_assertion are required", nil)
|
||||
return
|
||||
}
|
||||
|
||||
sess, err := s.sessionManager.CreateSession(r.Context(), req.TenantID, req.UserID, req.AuthAssertion)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to create session", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := CreateSessionResponse{
|
||||
SessionID: sess.ID,
|
||||
EphemeralToken: sess.EphemeralToken,
|
||||
Config: sess.Config,
|
||||
ExpiresAt: sess.ExpiresAt,
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, resp)
|
||||
}
|
||||
|
||||
// RefreshTokenResponse represents a token refresh response
|
||||
type RefreshTokenResponse struct {
|
||||
EphemeralToken string `json:"ephemeral_token"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
// handleRefreshToken handles POST /v1/sessions/:id/refresh-token
|
||||
func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
sessionID := vars["id"]
|
||||
|
||||
if sessionID == "" {
|
||||
writeError(w, http.StatusBadRequest, "session_id is required", nil)
|
||||
return
|
||||
}
|
||||
|
||||
newToken, err := s.sessionManager.RefreshToken(r.Context(), sessionID)
|
||||
if err != nil {
|
||||
if err.Error() == "session expired" {
|
||||
writeError(w, http.StatusUnauthorized, "session expired", err)
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, "failed to refresh token", err)
|
||||
return
|
||||
}
|
||||
|
||||
sess, err := s.sessionManager.GetSession(r.Context(), sessionID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to get session", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := RefreshTokenResponse{
|
||||
EphemeralToken: newToken,
|
||||
ExpiresAt: sess.ExpiresAt,
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// handleEndSession handles POST /v1/sessions/:id/end
|
||||
func (s *Server) handleEndSession(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
sessionID := vars["id"]
|
||||
|
||||
if sessionID == "" {
|
||||
writeError(w, http.StatusBadRequest, "session_id is required", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.sessionManager.EndSession(r.Context(), sessionID); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to end session", err)
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ended"})
|
||||
}
|
||||
|
||||
// handleHealth handles GET /health
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "healthy"})
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.router.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// writeJSON writes a JSON response
|
||||
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
// ErrorResponse represents an error response
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// writeError writes an error response
|
||||
func writeError(w http.ResponseWriter, status int, message string, err error) {
|
||||
resp := ErrorResponse{
|
||||
Error: message,
|
||||
Message: func() string {
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
}
|
||||
writeJSON(w, status, resp)
|
||||
}
|
||||
Reference in New Issue
Block a user