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:
2026-07-07 23:15:37 +08:00
commit 44406b72db
349 changed files with 73265 additions and 0 deletions
@@ -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))
}
}