Files
moteva/server/internal/handler/user_context_middleware.go
T

195 lines
6.4 KiB
Go

package handler
import (
"context"
"errors"
"net/http"
"strings"
"img_infinite_canvas/internal/domain/design"
authmodule "img_infinite_canvas/internal/modules/auth"
sharingmodule "img_infinite_canvas/internal/modules/sharing"
)
type bearerAuthenticator interface {
UserIDFromBearer(ctx context.Context, authorization string) (string, bool)
}
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]
}
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userID := ""
authenticated := false
if authenticator != nil {
if tokenUserID, ok := userIDFromRequestAuth(r.Context(), r, authenticator); ok {
userID = tokenUserID
authenticated = true
}
}
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 {
writeAuthRequired(w)
return
}
next(w, r.WithContext(ctx))
}
}
}
func userIDFromRequestAuth(ctx context.Context, r *http.Request, authenticator bearerAuthenticator) (string, bool) {
return authenticator.UserIDFromBearer(ctx, r.Header.Get("Authorization"))
}
func requiresAuthenticatedUser(r *http.Request) bool {
path := r.URL.Path
if strings.HasPrefix(path, "/v1/") {
return true
}
if !strings.HasPrefix(path, "/api/") {
return false
}
if strings.HasPrefix(path, "/api/auth/") ||
strings.HasPrefix(path, "/api/shares/") ||
path == "/api/auth/options" ||
path == "/api/health" ||
path == "/api/dashboard" {
return false
}
return true
}
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 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}`))
}
}
func LegacyUserContextMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := design.ContextWithUserID(r.Context(), "")
next(w, r.WithContext(ctx))
}
}