Initial commit: img-infinite-canvas AI design workbench MVP
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>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user