2026-07-07 23:15:37 +08:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-07-10 14:46:18 +08:00
|
|
|
"errors"
|
2026-07-07 23:15:37 +08:00
|
|
|
"net/http"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"img_infinite_canvas/internal/domain/design"
|
2026-07-10 14:46:18 +08:00
|
|
|
authmodule "img_infinite_canvas/internal/modules/auth"
|
|
|
|
|
sharingmodule "img_infinite_canvas/internal/modules/sharing"
|
2026-07-07 23:15:37 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type bearerAuthenticator interface {
|
|
|
|
|
UserIDFromBearer(ctx context.Context, authorization string) (string, bool)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-10 14:46:18 +08:00
|
|
|
type profileAuthenticator interface {
|
|
|
|
|
GetProfile(ctx context.Context, userID string) (authmodule.User, error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type shareRequestAuthorizer interface {
|
|
|
|
|
ResolveAccess(ctx context.Context, shareID string, actor sharingmodule.Actor) (sharingmodule.Access, error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func UserContextMiddleware(authenticator bearerAuthenticator, shareAuthorizers ...shareRequestAuthorizer) func(http.HandlerFunc) http.HandlerFunc {
|
|
|
|
|
var shareAuthorizer shareRequestAuthorizer
|
|
|
|
|
if len(shareAuthorizers) > 0 {
|
|
|
|
|
shareAuthorizer = shareAuthorizers[0]
|
|
|
|
|
}
|
2026-07-07 23:15:37 +08:00
|
|
|
return func(next http.HandlerFunc) http.HandlerFunc {
|
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
2026-07-10 14:46:18 +08:00
|
|
|
userID := ""
|
2026-07-07 23:15:37 +08:00
|
|
|
authenticated := false
|
|
|
|
|
if authenticator != nil {
|
|
|
|
|
if tokenUserID, ok := userIDFromRequestAuth(r.Context(), r, authenticator); ok {
|
|
|
|
|
userID = tokenUserID
|
|
|
|
|
authenticated = true
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-10 14:46:18 +08:00
|
|
|
actor := sharingmodule.Actor{Authenticated: authenticated, UserID: userID}
|
|
|
|
|
if authenticated && requestUsesShareAccess(r) {
|
|
|
|
|
if profiles, ok := authenticator.(profileAuthenticator); ok {
|
|
|
|
|
profile, err := profiles.GetProfile(r.Context(), userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeAuthRequired(w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
actor.Email = profile.Email
|
|
|
|
|
actor.CountryCode = profile.CountryCode
|
|
|
|
|
actor.Phone = profile.Phone
|
|
|
|
|
actor.Name = profile.Name
|
|
|
|
|
actor.AvatarURL = profile.AvatarURL
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx := sharingmodule.ContextWithActor(r.Context(), actor)
|
|
|
|
|
ctx = design.ContextWithUserID(ctx, userID)
|
|
|
|
|
shareAuthorized := false
|
|
|
|
|
shareID := strings.TrimSpace(r.Header.Get("X-Share-Id"))
|
|
|
|
|
projectID := shareProjectID(r)
|
|
|
|
|
if shareID != "" && r.Method == http.MethodDelete && r.URL.Path == "/api/assets" {
|
|
|
|
|
writeShareAccessError(w, sharingmodule.ErrForbidden)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if shareID != "" && projectID != "" && shareAuthorizer != nil {
|
|
|
|
|
access, err := shareAuthorizer.ResolveAccess(ctx, shareID, actor)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeShareAccessError(w, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if access.ProjectID != projectID {
|
|
|
|
|
writeShareAccessError(w, sharingmodule.ErrNotFound)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !access.Allows(requiredShareCapability(r)) {
|
|
|
|
|
writeShareAccessError(w, sharingmodule.ErrForbidden)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ctx = sharingmodule.ContextWithAccess(ctx, access)
|
|
|
|
|
ctx = design.ContextWithUserID(ctx, access.OwnerID)
|
|
|
|
|
shareAuthorized = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if requiresAuthenticatedUser(r) && !authenticated && !shareAuthorized {
|
2026-07-07 23:15:37 +08:00
|
|
|
writeAuthRequired(w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
next(w, r.WithContext(ctx))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func userIDFromRequestAuth(ctx context.Context, r *http.Request, authenticator bearerAuthenticator) (string, bool) {
|
2026-07-09 01:09:39 +08:00
|
|
|
return authenticator.UserIDFromBearer(ctx, r.Header.Get("Authorization"))
|
2026-07-07 23:15:37 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func requiresAuthenticatedUser(r *http.Request) bool {
|
|
|
|
|
path := r.URL.Path
|
2026-07-10 14:46:18 +08:00
|
|
|
if strings.HasPrefix(path, "/v1/") {
|
|
|
|
|
return true
|
|
|
|
|
}
|
2026-07-07 23:15:37 +08:00
|
|
|
if !strings.HasPrefix(path, "/api/") {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-07-10 14:46:18 +08:00
|
|
|
if strings.HasPrefix(path, "/api/auth/") ||
|
|
|
|
|
strings.HasPrefix(path, "/api/shares/") ||
|
|
|
|
|
path == "/api/auth/options" ||
|
|
|
|
|
path == "/api/health" ||
|
|
|
|
|
path == "/api/dashboard" {
|
2026-07-07 23:15:37 +08:00
|
|
|
return false
|
|
|
|
|
}
|
2026-07-10 14:46:18 +08:00
|
|
|
return true
|
2026-07-07 23:15:37 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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}`))
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-10 14:46:18 +08:00
|
|
|
func requestUsesShareAccess(r *http.Request) bool {
|
|
|
|
|
return strings.TrimSpace(r.Header.Get("X-Share-Id")) != "" || strings.HasPrefix(r.URL.Path, "/api/shares/")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func shareProjectID(r *http.Request) string {
|
|
|
|
|
const prefix = "/api/projects/"
|
|
|
|
|
if strings.HasPrefix(r.URL.Path, prefix) {
|
|
|
|
|
remainder := strings.TrimPrefix(r.URL.Path, prefix)
|
|
|
|
|
projectID, _, _ := strings.Cut(remainder, "/")
|
|
|
|
|
projectID = strings.TrimSpace(projectID)
|
|
|
|
|
if projectID == "" || projectID == "async" || projectID == "batch-delete" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return projectID
|
|
|
|
|
}
|
|
|
|
|
if r.URL.Path == "/api/generator/tasks" {
|
|
|
|
|
if headerProjectID := strings.TrimSpace(r.Header.Get("X-Share-Project-Id")); headerProjectID != "" {
|
|
|
|
|
return headerProjectID
|
|
|
|
|
}
|
|
|
|
|
return strings.TrimSpace(r.URL.Query().Get("project_id"))
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func requiredShareCapability(r *http.Request) sharingmodule.Capability {
|
|
|
|
|
path := r.URL.Path
|
|
|
|
|
if path == "/api/generator/tasks" {
|
|
|
|
|
return sharingmodule.CapabilityEdit
|
|
|
|
|
}
|
|
|
|
|
if strings.Contains(path, "/sharing") || (r.Method == http.MethodDelete && shareProjectRoot(path)) {
|
|
|
|
|
return sharingmodule.CapabilityManage
|
|
|
|
|
}
|
|
|
|
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
|
|
|
|
return sharingmodule.CapabilityEdit
|
|
|
|
|
}
|
|
|
|
|
if strings.HasSuffix(path, "/agent/ws") {
|
|
|
|
|
return sharingmodule.CapabilityEdit
|
|
|
|
|
}
|
|
|
|
|
if strings.Contains(path, "/agent/") || strings.HasSuffix(path, "/history") || strings.HasSuffix(path, "/events") {
|
|
|
|
|
return sharingmodule.CapabilityViewChat
|
|
|
|
|
}
|
|
|
|
|
return sharingmodule.CapabilityViewCanvas
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func shareProjectRoot(path string) bool {
|
|
|
|
|
remainder := strings.TrimPrefix(path, "/api/projects/")
|
|
|
|
|
return remainder != path && !strings.Contains(remainder, "/")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeShareAccessError(w http.ResponseWriter, err error) {
|
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
|
switch {
|
|
|
|
|
case errors.Is(err, sharingmodule.ErrLoginRequired):
|
|
|
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
|
|
|
_, _ = w.Write([]byte(`{"error":"login required for edit access","errorCode":"share.login_required","code":401}`))
|
|
|
|
|
case errors.Is(err, sharingmodule.ErrForbidden):
|
|
|
|
|
w.WriteHeader(http.StatusForbidden)
|
|
|
|
|
_, _ = w.Write([]byte(`{"error":"share access forbidden","errorCode":"share.forbidden","code":403}`))
|
|
|
|
|
case errors.Is(err, sharingmodule.ErrNotFound):
|
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
|
|
|
|
_, _ = w.Write([]byte(`{"error":"share not found","errorCode":"share.not_found","code":404}`))
|
|
|
|
|
default:
|
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
_, _ = w.Write([]byte(`{"error":"share authorization failed","errorCode":"share.internal","code":500}`))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 23:15:37 +08:00
|
|
|
func LegacyUserContextMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
2026-07-10 14:46:18 +08:00
|
|
|
ctx := design.ContextWithUserID(r.Context(), "")
|
2026-07-07 23:15:37 +08:00
|
|
|
next(w, r.WithContext(ctx))
|
|
|
|
|
}
|
|
|
|
|
}
|