44406b72db
Moteva-style AI design workbench replica. Home prompt creates a project; the project page provides an infinite canvas with node drag, zoom/pan, a design chat panel, history replay, image asset upload, and project save/regenerate. - Backend: go-zero, DDD layering, sqlc/pgx, optional PostgreSQL; memory or Redis cache; asynq job queue; MinIO/S3/R2/OSS object storage; sky-valley/pi agent runtime adapter. - Frontend: Next.js App Router + Vite artifact build, TypeScript, i18n, shadcn/ui components, auth (OTP/Turnstile/Google/WeChat). - Deploy: Docker Compose and k3s manifests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
80 lines
2.5 KiB
Go
80 lines
2.5 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"img_infinite_canvas/internal/domain/design"
|
|
)
|
|
|
|
type bearerAuthenticator interface {
|
|
UserIDFromBearer(ctx context.Context, authorization string) (string, bool)
|
|
}
|
|
|
|
func UserContextMiddleware(authenticator bearerAuthenticator) func(http.HandlerFunc) http.HandlerFunc {
|
|
return func(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
userID := r.Header.Get("X-User-Id")
|
|
authenticated := false
|
|
if authenticator != nil {
|
|
if tokenUserID, ok := userIDFromRequestAuth(r.Context(), r, authenticator); ok {
|
|
userID = tokenUserID
|
|
authenticated = true
|
|
}
|
|
}
|
|
if requiresAuthenticatedUser(r) && !authenticated {
|
|
writeAuthRequired(w)
|
|
return
|
|
}
|
|
ctx := design.ContextWithUserID(r.Context(), userID)
|
|
next(w, r.WithContext(ctx))
|
|
}
|
|
}
|
|
}
|
|
|
|
func userIDFromRequestAuth(ctx context.Context, r *http.Request, authenticator bearerAuthenticator) (string, bool) {
|
|
if tokenUserID, ok := authenticator.UserIDFromBearer(ctx, r.Header.Get("Authorization")); ok {
|
|
return tokenUserID, true
|
|
}
|
|
if token := strings.TrimSpace(r.Header.Get("token")); token != "" {
|
|
if tokenUserID, ok := authenticator.UserIDFromBearer(ctx, "Bearer "+token); ok {
|
|
return tokenUserID, true
|
|
}
|
|
}
|
|
if cookie, err := r.Cookie("usertoken"); err == nil && strings.TrimSpace(cookie.Value) != "" {
|
|
if tokenUserID, ok := authenticator.UserIDFromBearer(ctx, "Bearer "+cookie.Value); ok {
|
|
return tokenUserID, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func requiresAuthenticatedUser(r *http.Request) bool {
|
|
path := r.URL.Path
|
|
if !strings.HasPrefix(path, "/api/") {
|
|
return false
|
|
}
|
|
if strings.HasPrefix(path, "/api/auth/") || path == "/api/auth/options" || path == "/api/health" || path == "/api/dashboard" {
|
|
return false
|
|
}
|
|
return strings.HasPrefix(path, "/api/projects") ||
|
|
strings.HasPrefix(path, "/api/account") ||
|
|
strings.HasPrefix(path, "/api/assets") ||
|
|
strings.HasPrefix(path, "/api/generator/")
|
|
}
|
|
|
|
func writeAuthRequired(w http.ResponseWriter) {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
_, _ = w.Write([]byte(`{"error":"login required","errorCode":"auth.login_required","code":401}`))
|
|
}
|
|
|
|
func LegacyUserContextMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
userID := r.Header.Get("X-User-Id")
|
|
ctx := design.ContextWithUserID(r.Context(), userID)
|
|
next(w, r.WithContext(ctx))
|
|
}
|
|
}
|