diff --git a/.dockerignore b/.dockerignore index 804ab223c..8dac96d50 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,7 @@ **/.classpath **/.dockerignore **/.env +!frontend/.env **/.git **/.gitignore **/.project diff --git a/backend/app/api/handlers/v1/controller.go b/backend/app/api/handlers/v1/controller.go index b4e78e810..d24e1b228 100644 --- a/backend/app/api/handlers/v1/controller.go +++ b/backend/app/api/handlers/v1/controller.go @@ -133,7 +133,7 @@ func NewControllerV1(svc *services.AllServices, repos *repo.AllRepos, bus *event func (ctrl *V1Controller) initOIDCProvider() { if ctrl.config.OIDC.Enabled { - oidcProvider, err := providers.NewOIDCProvider(ctrl.svc.User, &ctrl.config.OIDC, &ctrl.config.Options, ctrl.cookieSecure) + oidcProvider, err := providers.NewOIDCProvider(ctrl.svc.User, &ctrl.config.OIDC, &ctrl.config.Options, &ctrl.config.Web, ctrl.cookieSecure) if err != nil { log.Err(err).Msg("failed to initialize OIDC provider at startup") } else { diff --git a/backend/app/api/handlers/v1/v1_ctrl_auth.go b/backend/app/api/handlers/v1/v1_ctrl_auth.go index cc9078eac..eabc5119a 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_auth.go +++ b/backend/app/api/handlers/v1/v1_ctrl_auth.go @@ -337,13 +337,13 @@ func (ctrl *V1Controller) HandleOIDCCallback() errchain.HandlerFunc { newToken, err := ctrl.oidcProvider.HandleCallback(w, r) if err != nil { log.Err(err).Msg("OIDC callback failed") - http.Redirect(w, r, "/?oidc_error=oidc_auth_failed", http.StatusFound) + http.Redirect(w, r, ctrl.config.Web.AppBase+"?oidc_error=oidc_auth_failed", http.StatusFound) return nil } // Set cookies and redirect to home ctrl.setCookies(w, noPort(r.Host), newToken.Raw, newToken.ExpiresAt, true, newToken.AttachmentToken) - http.Redirect(w, r, "/home", http.StatusFound) + http.Redirect(w, r, ctrl.config.Web.AppBase+"home", http.StatusFound) return nil } } diff --git a/backend/app/api/providers/oidc.go b/backend/app/api/providers/oidc.go index ae64cbbc4..4ed336884 100644 --- a/backend/app/api/providers/oidc.go +++ b/backend/app/api/providers/oidc.go @@ -24,6 +24,7 @@ type OIDCProvider struct { service *services.UserService config *config.OIDCConf options *config.Options + webConfig *config.WebConfig cookieSecure bool provider *oidc.Provider verifier *oidc.IDTokenVerifier @@ -39,7 +40,7 @@ type OIDCClaims struct { EmailVerified *bool } -func NewOIDCProvider(service *services.UserService, config *config.OIDCConf, options *config.Options, cookieSecure bool) (*OIDCProvider, error) { +func NewOIDCProvider(service *services.UserService, config *config.OIDCConf, options *config.Options, webConfig *config.WebConfig, cookieSecure bool) (*OIDCProvider, error) { if !config.Enabled { return nil, fmt.Errorf("OIDC is not enabled") } @@ -106,6 +107,7 @@ func NewOIDCProvider(service *services.UserService, config *config.OIDCConf, opt service: service, config: config, options: options, + webConfig: webConfig, cookieSecure: cookieSecure, provider: provider, verifier: verifier, @@ -416,7 +418,7 @@ func (p *OIDCProvider) initiateOIDCFlow(w http.ResponseWriter, r *http.Request) Domain: domain, Secure: p.isSecure(r), HttpOnly: true, - Path: "/", + Path: u.Path, SameSite: http.SameSiteLaxMode, }) @@ -428,7 +430,7 @@ func (p *OIDCProvider) initiateOIDCFlow(w http.ResponseWriter, r *http.Request) Domain: domain, Secure: p.isSecure(r), HttpOnly: true, - Path: "/", + Path: u.Path, SameSite: http.SameSiteLaxMode, }) @@ -440,7 +442,7 @@ func (p *OIDCProvider) initiateOIDCFlow(w http.ResponseWriter, r *http.Request) Domain: domain, Secure: p.isSecure(r), HttpOnly: true, - Path: "/", + Path: u.Path, SameSite: http.SameSiteLaxMode, }) @@ -470,7 +472,7 @@ func (p *OIDCProvider) handleCallback(w http.ResponseWriter, r *http.Request) (s MaxAge: -1, Secure: p.isSecure(r), HttpOnly: true, - Path: "/", + Path: u.Path, SameSite: http.SameSiteLaxMode, }) http.SetCookie(w, &http.Cookie{ @@ -481,7 +483,7 @@ func (p *OIDCProvider) handleCallback(w http.ResponseWriter, r *http.Request) (s MaxAge: -1, Secure: p.isSecure(r), HttpOnly: true, - Path: "/", + Path: u.Path, SameSite: http.SameSiteLaxMode, }) http.SetCookie(w, &http.Cookie{ @@ -492,7 +494,7 @@ func (p *OIDCProvider) handleCallback(w http.ResponseWriter, r *http.Request) (s MaxAge: -1, Secure: p.isSecure(r), HttpOnly: true, - Path: "/", + Path: u.Path, SameSite: http.SameSiteLaxMode, }) } @@ -600,7 +602,7 @@ func (p *OIDCProvider) getBaseURL(r *http.Request) string { } } - return scheme + "://" + host + return scheme + "://" + host + p.webConfig.AppBase } func (p *OIDCProvider) isSecure(r *http.Request) bool { diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go index 355f30fda..984054d2c 100644 --- a/backend/app/api/routes.go +++ b/backend/app/api/routes.go @@ -59,7 +59,7 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR v1.WithURL(fmt.Sprintf("%s:%s", a.conf.Web.Host, a.conf.Web.Port)), ) - r.Route(prefix+"/v1", func(r chi.Router) { + r.Route(path.Join(a.conf.Web.AppBase, prefix, "/v1"), func(r chi.Router) { r.Get("/status", chain.ToHandlerFunc(v1Ctrl.HandleBase(func() bool { return true }, v1.Build{ Version: version, Commit: commit, diff --git a/backend/internal/sys/config/conf.go b/backend/internal/sys/config/conf.go index 4750c8d18..82fcac6f2 100644 --- a/backend/internal/sys/config/conf.go +++ b/backend/internal/sys/config/conf.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "os" + "strings" "time" "github.com/ardanlabs/conf/v3" @@ -61,6 +62,7 @@ type DebugConf struct { type WebConfig struct { Port string `yaml:"port" conf:"default:7745"` Host string `yaml:"host"` + AppBase string `yaml:"app_base" conf:"default:/"` MaxUploadSize int64 `yaml:"max_file_upload" conf:"default:10"` ReadTimeout time.Duration `yaml:"read_timeout" conf:"default:10s"` WriteTimeout time.Duration `yaml:"write_timeout" conf:"default:10s"` @@ -137,9 +139,27 @@ func New(buildstr string, description string) (*Config, error) { return &cfg, fmt.Errorf("parsing config: %w", err) } + cfg.Web.AppBase = normalizePath(cfg.Web.AppBase) + return &cfg, nil } +// normalizePath ensures a path is never empty and always has a leading and +// trailing slash. This prevents malformed URL concatenation when AppBase is +// used as a path prefix. +func normalizePath(p string) string { + if p == "" { + return "/" + } + if !strings.HasPrefix(p, "/") { + p = "/" + p + } + if !strings.HasSuffix(p, "/") { + p = p + "/" + } + return p +} + // Print prints the configuration to stdout as a json indented string // This is useful for debugging. If the marshaller errors out, it will panic. func (c *Config) Print() { diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 000000000..a76dc62c4 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +NUXT_APP_BASE_URL=/ diff --git a/frontend/components/Collection/JoinModal.vue b/frontend/components/Collection/JoinModal.vue index 43dbb6c79..6f5b5c588 100644 --- a/frontend/components/Collection/JoinModal.vue +++ b/frontend/components/Collection/JoinModal.vue @@ -15,7 +15,7 @@