mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-01-31 19:52:21 +00:00
Fix UI and backend paths with subpath (#1799)
I'm not sure if this is an ideal fix for this, but it seems to work for me. If you have another idea just let me know. Closes #1798 Closes #1773
This commit is contained in:
parent
10b1cfcd3b
commit
67b7de5cc2
30 changed files with 162 additions and 101 deletions
|
@ -61,8 +61,8 @@ var flags = []cli.Flag{
|
||||||
Usage: "server fully qualified url for forge's Webhooks (<scheme>://<host>)",
|
Usage: "server fully qualified url for forge's Webhooks (<scheme>://<host>)",
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
EnvVars: []string{"WOODPECKER_ROOT_URL"},
|
EnvVars: []string{"WOODPECKER_ROOT_PATH", "WOODPECKER_ROOT_URL"},
|
||||||
Name: "root-url",
|
Name: "root-path",
|
||||||
Usage: "server url root (used for statics loading when having a url path prefix)",
|
Usage: "server url root (used for statics loading when having a url path prefix)",
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
|
|
|
@ -357,7 +357,11 @@ func setupEvilGlobals(c *cli.Context, v store.Store, f forge.Forge) {
|
||||||
server.Config.Server.StatusContext = c.String("status-context")
|
server.Config.Server.StatusContext = c.String("status-context")
|
||||||
server.Config.Server.StatusContextFormat = c.String("status-context-format")
|
server.Config.Server.StatusContextFormat = c.String("status-context-format")
|
||||||
server.Config.Server.SessionExpires = c.Duration("session-expires")
|
server.Config.Server.SessionExpires = c.Duration("session-expires")
|
||||||
server.Config.Server.RootURL = strings.TrimSuffix(c.String("root-url"), "/")
|
rootPath := strings.TrimSuffix(c.String("root-path"), "/")
|
||||||
|
if rootPath != "" && !strings.HasPrefix(rootPath, "/") {
|
||||||
|
rootPath = "/" + rootPath
|
||||||
|
}
|
||||||
|
server.Config.Server.RootPath = rootPath
|
||||||
server.Config.Server.CustomCSSFile = strings.TrimSpace(c.String("custom-css-file"))
|
server.Config.Server.CustomCSSFile = strings.TrimSpace(c.String("custom-css-file"))
|
||||||
server.Config.Server.CustomJsFile = strings.TrimSpace(c.String("custom-js-file"))
|
server.Config.Server.CustomJsFile = strings.TrimSpace(c.String("custom-js-file"))
|
||||||
server.Config.Pipeline.Networks = c.StringSlice("network")
|
server.Config.Pipeline.Networks = c.StringSlice("network")
|
||||||
|
|
|
@ -193,4 +193,4 @@ A [Prometheus endpoint](./90-prometheus.md) is exposed.
|
||||||
|
|
||||||
See the [proxy guide](./70-proxy.md) if you want to see a setup behind Apache, Nginx, Caddy or ngrok.
|
See the [proxy guide](./70-proxy.md) if you want to see a setup behind Apache, Nginx, Caddy or ngrok.
|
||||||
|
|
||||||
In the case you need to use Woodpecker with a URL path prefix (like: https://example.org/woodpecker/), you can use the option [`WOODPECKER_ROOT_URL`](./10-server-config.md#woodpecker_root_url).
|
In the case you need to use Woodpecker with a URL path prefix (like: https://example.org/woodpecker/), you can use the option [`WOODPECKER_ROOT_PATH`](./10-server-config.md#woodpecker_root_path).
|
||||||
|
|
|
@ -528,12 +528,12 @@ Specify a configuration service endpoint, see [Configuration Extension](./100-ex
|
||||||
|
|
||||||
Specify how many seconds before timeout when fetching the Woodpecker configuration from a Forge
|
Specify how many seconds before timeout when fetching the Woodpecker configuration from a Forge
|
||||||
|
|
||||||
### `WOODPECKER_ROOT_URL`
|
### `WOODPECKER_ROOT_PATH`
|
||||||
> Default: ``
|
> Default: ``
|
||||||
|
|
||||||
Server URL path prefix (used for statics loading when having a url path prefix), should start with `/`
|
Server URL path prefix (used for statics loading when having a url path prefix), should start with `/`
|
||||||
|
|
||||||
Example: `WOODPECKER_ROOT_URL=/woodpecker`
|
Example: `WOODPECKER_ROOT_PATH=/woodpecker`
|
||||||
|
|
||||||
### `WOODPECKER_ENABLE_SWAGGER`
|
### `WOODPECKER_ENABLE_SWAGGER`
|
||||||
> Default: true
|
> Default: true
|
||||||
|
|
|
@ -34,14 +34,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleLogin(c *gin.Context) {
|
func HandleLogin(c *gin.Context) {
|
||||||
var (
|
if err := c.Request.FormValue("error"); err != "" {
|
||||||
w = c.Writer
|
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login/error?code="+err)
|
||||||
r = c.Request
|
|
||||||
)
|
|
||||||
if err := r.FormValue("error"); err != "" {
|
|
||||||
http.Redirect(w, r, "/login/error?code="+err, 303)
|
|
||||||
} else {
|
} else {
|
||||||
http.Redirect(w, r, "/authorize", 303)
|
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/authorize")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +52,7 @@ func HandleAuth(c *gin.Context) {
|
||||||
tmpuser, err := _forge.Login(c, c.Writer, c.Request)
|
tmpuser, err := _forge.Login(c, c.Writer, c.Request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msgf("cannot authenticate user. %s", err)
|
log.Error().Msgf("cannot authenticate user. %s", err)
|
||||||
c.Redirect(http.StatusSeeOther, "/login?error=oauth_error")
|
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=oauth_error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// this will happen when the user is redirected by the forge as
|
// this will happen when the user is redirected by the forge as
|
||||||
|
@ -77,7 +73,7 @@ func HandleAuth(c *gin.Context) {
|
||||||
// if self-registration is disabled we should return a not authorized error
|
// if self-registration is disabled we should return a not authorized error
|
||||||
if !config.Open && !config.IsAdmin(tmpuser) {
|
if !config.Open && !config.IsAdmin(tmpuser) {
|
||||||
log.Error().Msgf("cannot register %s. registration closed", tmpuser.Login)
|
log.Error().Msgf("cannot register %s. registration closed", tmpuser.Login)
|
||||||
c.Redirect(http.StatusSeeOther, "/login?error=access_denied")
|
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=access_denied")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +83,7 @@ func HandleAuth(c *gin.Context) {
|
||||||
teams, terr := _forge.Teams(c, tmpuser)
|
teams, terr := _forge.Teams(c, tmpuser)
|
||||||
if terr != nil || !config.IsMember(teams) {
|
if terr != nil || !config.IsMember(teams) {
|
||||||
log.Error().Err(terr).Msgf("cannot verify team membership for %s.", u.Login)
|
log.Error().Err(terr).Msgf("cannot verify team membership for %s.", u.Login)
|
||||||
c.Redirect(303, "/login?error=access_denied")
|
c.Redirect(303, server.Config.Server.RootPath+"/login?error=access_denied")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,7 +104,7 @@ func HandleAuth(c *gin.Context) {
|
||||||
// insert the user into the database
|
// insert the user into the database
|
||||||
if err := _store.CreateUser(u); err != nil {
|
if err := _store.CreateUser(u); err != nil {
|
||||||
log.Error().Msgf("cannot insert %s. %s", u.Login, err)
|
log.Error().Msgf("cannot insert %s. %s", u.Login, err)
|
||||||
c.Redirect(http.StatusSeeOther, "/login?error=internal_error")
|
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,14 +133,14 @@ func HandleAuth(c *gin.Context) {
|
||||||
teams, terr := _forge.Teams(c, u)
|
teams, terr := _forge.Teams(c, u)
|
||||||
if terr != nil || !config.IsMember(teams) {
|
if terr != nil || !config.IsMember(teams) {
|
||||||
log.Error().Err(terr).Msgf("cannot verify team membership for %s.", u.Login)
|
log.Error().Err(terr).Msgf("cannot verify team membership for %s.", u.Login)
|
||||||
c.Redirect(http.StatusSeeOther, "/login?error=access_denied")
|
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=access_denied")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := _store.UpdateUser(u); err != nil {
|
if err := _store.UpdateUser(u); err != nil {
|
||||||
log.Error().Msgf("cannot update %s. %s", u.Login, err)
|
log.Error().Msgf("cannot update %s. %s", u.Login, err)
|
||||||
c.Redirect(http.StatusSeeOther, "/login?error=internal_error")
|
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,7 +148,7 @@ func HandleAuth(c *gin.Context) {
|
||||||
tokenString, err := token.New(token.SessToken, u.Login).SignExpires(u.Hash, exp)
|
tokenString, err := token.New(token.SessToken, u.Login).SignExpires(u.Hash, exp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msgf("cannot create token for %s. %s", u.Login, err)
|
log.Error().Msgf("cannot create token for %s. %s", u.Login, err)
|
||||||
c.Redirect(http.StatusSeeOther, "/login?error=internal_error")
|
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login?error=internal_error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,13 +183,13 @@ func HandleAuth(c *gin.Context) {
|
||||||
|
|
||||||
httputil.SetCookie(c.Writer, c.Request, "user_sess", tokenString)
|
httputil.SetCookie(c.Writer, c.Request, "user_sess", tokenString)
|
||||||
|
|
||||||
c.Redirect(http.StatusSeeOther, "/")
|
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/")
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetLogout(c *gin.Context) {
|
func GetLogout(c *gin.Context) {
|
||||||
httputil.DelCookie(c.Writer, c.Request, "user_sess")
|
httputil.DelCookie(c.Writer, c.Request, "user_sess")
|
||||||
httputil.DelCookie(c.Writer, c.Request, "user_last")
|
httputil.DelCookie(c.Writer, c.Request, "user_last")
|
||||||
c.Redirect(http.StatusSeeOther, "/")
|
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/")
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetLoginToken(c *gin.Context) {
|
func GetLoginToken(c *gin.Context) {
|
||||||
|
|
|
@ -67,7 +67,7 @@ var Config = struct {
|
||||||
StatusContext string
|
StatusContext string
|
||||||
StatusContextFormat string
|
StatusContextFormat string
|
||||||
SessionExpires time.Duration
|
SessionExpires time.Duration
|
||||||
RootURL string
|
RootPath string
|
||||||
CustomCSSFile string
|
CustomCSSFile string
|
||||||
CustomJsFile string
|
CustomJsFile string
|
||||||
Migrations struct {
|
Migrations struct {
|
||||||
|
|
|
@ -421,7 +421,7 @@ func (c *config) newOAuth2Config() *oauth2.Config {
|
||||||
AuthURL: fmt.Sprintf("%s/site/oauth2/authorize", c.url),
|
AuthURL: fmt.Sprintf("%s/site/oauth2/authorize", c.url),
|
||||||
TokenURL: fmt.Sprintf("%s/site/oauth2/access_token", c.url),
|
TokenURL: fmt.Sprintf("%s/site/oauth2/access_token", c.url),
|
||||||
},
|
},
|
||||||
RedirectURL: fmt.Sprintf("%s/authorize", server.Config.Server.OAuthHost),
|
RedirectURL: fmt.Sprintf("%s%s/authorize", server.Config.Server.OAuthHost, server.Config.Server.RootPath),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -103,7 +103,7 @@ func (c *Gitea) oauth2Config(ctx context.Context) (*oauth2.Config, context.Conte
|
||||||
AuthURL: fmt.Sprintf(authorizeTokenURL, c.url),
|
AuthURL: fmt.Sprintf(authorizeTokenURL, c.url),
|
||||||
TokenURL: fmt.Sprintf(accessTokenURL, c.url),
|
TokenURL: fmt.Sprintf(accessTokenURL, c.url),
|
||||||
},
|
},
|
||||||
RedirectURL: fmt.Sprintf("%s/authorize", server.Config.Server.OAuthHost),
|
RedirectURL: fmt.Sprintf("%s%s/authorize", server.Config.Server.OAuthHost, server.Config.Server.RootPath),
|
||||||
},
|
},
|
||||||
|
|
||||||
context.WithValue(ctx, oauth2.HTTPClient, &http.Client{Transport: &http.Transport{
|
context.WithValue(ctx, oauth2.HTTPClient, &http.Client{Transport: &http.Transport{
|
||||||
|
|
|
@ -395,9 +395,9 @@ func (c *client) newConfig(req *http.Request) *oauth2.Config {
|
||||||
|
|
||||||
intendedURL := req.URL.Query()["url"]
|
intendedURL := req.URL.Query()["url"]
|
||||||
if len(intendedURL) > 0 {
|
if len(intendedURL) > 0 {
|
||||||
redirect = fmt.Sprintf("%s/authorize?url=%s", server.Config.Server.OAuthHost, intendedURL[0])
|
redirect = fmt.Sprintf("%s%s/authorize?url=%s", server.Config.Server.OAuthHost, server.Config.Server.RootPath, intendedURL[0])
|
||||||
} else {
|
} else {
|
||||||
redirect = fmt.Sprintf("%s/authorize", server.Config.Server.OAuthHost)
|
redirect = fmt.Sprintf("%s%s/authorize", server.Config.Server.OAuthHost, server.Config.Server.RootPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &oauth2.Config{
|
return &oauth2.Config{
|
||||||
|
|
|
@ -93,7 +93,7 @@ func (g *GitLab) oauth2Config(ctx context.Context) (*oauth2.Config, context.Cont
|
||||||
TokenURL: fmt.Sprintf("%s/oauth/token", g.url),
|
TokenURL: fmt.Sprintf("%s/oauth/token", g.url),
|
||||||
},
|
},
|
||||||
Scopes: []string{defaultScope},
|
Scopes: []string{defaultScope},
|
||||||
RedirectURL: fmt.Sprintf("%s/authorize", server.Config.Server.OAuthHost),
|
RedirectURL: fmt.Sprintf("%s%s/authorize", server.Config.Server.OAuthHost, server.Config.Server.RootPath),
|
||||||
},
|
},
|
||||||
|
|
||||||
context.WithValue(ctx, oauth2.HTTPClient, &http.Client{Transport: &http.Transport{
|
context.WithValue(ctx, oauth2.HTTPClient, &http.Client{Transport: &http.Transport{
|
||||||
|
|
|
@ -23,7 +23,7 @@ import (
|
||||||
"github.com/woodpecker-ci/woodpecker/server/router/middleware/session"
|
"github.com/woodpecker-ci/woodpecker/server/router/middleware/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
func apiRoutes(e *gin.Engine) {
|
func apiRoutes(e *gin.RouterGroup) {
|
||||||
apiBase := e.Group("/api")
|
apiBase := e.Group("/api")
|
||||||
{
|
{
|
||||||
user := apiBase.Group("/user")
|
user := apiBase.Group("/user")
|
||||||
|
|
|
@ -22,9 +22,9 @@ import (
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
swaggerfiles "github.com/swaggo/files"
|
swaggerfiles "github.com/swaggo/files"
|
||||||
ginSwagger "github.com/swaggo/gin-swagger"
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
|
|
||||||
"github.com/woodpecker-ci/woodpecker/cmd/server/docs"
|
"github.com/woodpecker-ci/woodpecker/cmd/server/docs"
|
||||||
"github.com/woodpecker-ci/woodpecker/server"
|
"github.com/woodpecker-ci/woodpecker/server"
|
||||||
|
|
||||||
"github.com/woodpecker-ci/woodpecker/server/api"
|
"github.com/woodpecker-ci/woodpecker/server/api"
|
||||||
"github.com/woodpecker-ci/woodpecker/server/api/metrics"
|
"github.com/woodpecker-ci/woodpecker/server/api/metrics"
|
||||||
"github.com/woodpecker-ci/woodpecker/server/router/middleware/header"
|
"github.com/woodpecker-ci/woodpecker/server/router/middleware/header"
|
||||||
|
@ -53,22 +53,25 @@ func Load(noRouteHandler http.HandlerFunc, middleware ...gin.HandlerFunc) http.H
|
||||||
|
|
||||||
e.NoRoute(gin.WrapF(noRouteHandler))
|
e.NoRoute(gin.WrapF(noRouteHandler))
|
||||||
|
|
||||||
e.GET("/web-config.js", web.Config)
|
base := e.Group(server.Config.Server.RootPath)
|
||||||
|
|
||||||
e.GET("/logout", api.GetLogout)
|
|
||||||
e.GET("/login", api.HandleLogin)
|
|
||||||
auth := e.Group("/authorize")
|
|
||||||
{
|
{
|
||||||
auth.GET("", api.HandleAuth)
|
base.GET("/web-config.js", web.Config)
|
||||||
auth.POST("", api.HandleAuth)
|
|
||||||
auth.POST("/token", api.GetLoginToken)
|
base.GET("/logout", api.GetLogout)
|
||||||
|
base.GET("/login", api.HandleLogin)
|
||||||
|
auth := base.Group("/authorize")
|
||||||
|
{
|
||||||
|
auth.GET("", api.HandleAuth)
|
||||||
|
auth.POST("", api.HandleAuth)
|
||||||
|
auth.POST("/token", api.GetLoginToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
base.GET("/metrics", metrics.PromHandler())
|
||||||
|
base.GET("/version", api.Version)
|
||||||
|
base.GET("/healthz", api.Health)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.GET("/metrics", metrics.PromHandler())
|
apiRoutes(base)
|
||||||
e.GET("/version", api.Version)
|
|
||||||
e.GET("/healthz", api.Health)
|
|
||||||
|
|
||||||
apiRoutes(e)
|
|
||||||
if server.Config.Server.EnableSwagger {
|
if server.Config.Server.EnableSwagger {
|
||||||
setupSwaggerConfigAndRoutes(e)
|
setupSwaggerConfigAndRoutes(e)
|
||||||
}
|
}
|
||||||
|
@ -78,8 +81,8 @@ func Load(noRouteHandler http.HandlerFunc, middleware ...gin.HandlerFunc) http.H
|
||||||
|
|
||||||
func setupSwaggerConfigAndRoutes(e *gin.Engine) {
|
func setupSwaggerConfigAndRoutes(e *gin.Engine) {
|
||||||
docs.SwaggerInfo.Host = getHost(server.Config.Server.Host)
|
docs.SwaggerInfo.Host = getHost(server.Config.Server.Host)
|
||||||
docs.SwaggerInfo.BasePath = server.Config.Server.RootURL + "/api"
|
docs.SwaggerInfo.BasePath = server.Config.Server.RootPath + "/api"
|
||||||
e.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
|
e.GET(server.Config.Server.RootPath+"/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getHost(s string) string {
|
func getHost(s string) string {
|
||||||
|
|
|
@ -45,7 +45,7 @@ func Config(c *gin.Context) {
|
||||||
"docs": server.Config.Server.Docs,
|
"docs": server.Config.Server.Docs,
|
||||||
"version": version.String(),
|
"version": version.String(),
|
||||||
"forge": server.Config.Services.Forge.Name(),
|
"forge": server.Config.Services.Forge.Name(),
|
||||||
"root_url": server.Config.Server.RootURL,
|
"root_path": server.Config.Server.RootPath,
|
||||||
"enable_swagger": server.Config.Server.EnableSwagger,
|
"enable_swagger": server.Config.Server.EnableSwagger,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +75,6 @@ window.WOODPECKER_CSRF = "{{ .csrf }}";
|
||||||
window.WOODPECKER_VERSION = "{{ .version }}";
|
window.WOODPECKER_VERSION = "{{ .version }}";
|
||||||
window.WOODPECKER_DOCS = "{{ .docs }}";
|
window.WOODPECKER_DOCS = "{{ .docs }}";
|
||||||
window.WOODPECKER_FORGE = "{{ .forge }}";
|
window.WOODPECKER_FORGE = "{{ .forge }}";
|
||||||
window.WOODPECKER_ROOT_URL = "{{ .root_url }}";
|
window.WOODPECKER_ROOT_PATH = "{{ .root_path }}";
|
||||||
window.WOODPECKER_ENABLE_SWAGGER = {{ .enable_swagger }};
|
window.WOODPECKER_ENABLE_SWAGGER = {{ .enable_swagger }};
|
||||||
`
|
`
|
||||||
|
|
|
@ -17,10 +17,11 @@ package web
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -54,24 +55,23 @@ func New() (*gin.Engine, error) {
|
||||||
|
|
||||||
e.Use(setupCache)
|
e.Use(setupCache)
|
||||||
|
|
||||||
rootURL, _ := url.Parse(server.Config.Server.RootURL)
|
rootPath := server.Config.Server.RootPath
|
||||||
rootPath := rootURL.Path
|
|
||||||
|
|
||||||
httpFS, err := web.HTTPFS()
|
httpFS, err := web.HTTPFS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
h := http.FileServer(&prefixFS{httpFS, rootPath})
|
f := &prefixFS{httpFS, rootPath}
|
||||||
e.GET(rootPath+"/favicon.svg", redirect(server.Config.Server.RootURL+"/favicons/favicon-light-default.svg", http.StatusPermanentRedirect))
|
e.GET(rootPath+"/favicon.svg", redirect(server.Config.Server.RootPath+"/favicons/favicon-light-default.svg", http.StatusPermanentRedirect))
|
||||||
e.GET(rootPath+"/favicons/*filepath", gin.WrapH(h))
|
e.GET(rootPath+"/favicons/*filepath", serveFile(f))
|
||||||
e.GET(rootPath+"/assets/*filepath", gin.WrapH(handleCustomFilesAndAssets(h)))
|
e.GET(rootPath+"/assets/*filepath", handleCustomFilesAndAssets(f))
|
||||||
|
|
||||||
e.NoRoute(handleIndex)
|
e.NoRoute(handleIndex)
|
||||||
|
|
||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCustomFilesAndAssets(assetHandler http.Handler) http.HandlerFunc {
|
func handleCustomFilesAndAssets(fs *prefixFS) func(ctx *gin.Context) {
|
||||||
serveFileOrEmptyContent := func(w http.ResponseWriter, r *http.Request, localFileName string) {
|
serveFileOrEmptyContent := func(w http.ResponseWriter, r *http.Request, localFileName string) {
|
||||||
if len(localFileName) > 0 {
|
if len(localFileName) > 0 {
|
||||||
http.ServeFile(w, r, localFileName)
|
http.ServeFile(w, r, localFileName)
|
||||||
|
@ -80,13 +80,50 @@ func handleCustomFilesAndAssets(assetHandler http.Handler) http.HandlerFunc {
|
||||||
http.ServeContent(w, r, localFileName, time.Now(), bytes.NewReader([]byte{}))
|
http.ServeContent(w, r, localFileName, time.Now(), bytes.NewReader([]byte{}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(ctx *gin.Context) {
|
||||||
if strings.HasSuffix(r.RequestURI, "/assets/custom.js") {
|
if strings.HasSuffix(ctx.Request.RequestURI, "/assets/custom.js") {
|
||||||
serveFileOrEmptyContent(w, r, server.Config.Server.CustomJsFile)
|
serveFileOrEmptyContent(ctx.Writer, ctx.Request, server.Config.Server.CustomJsFile)
|
||||||
} else if strings.HasSuffix(r.RequestURI, "/assets/custom.css") {
|
} else if strings.HasSuffix(ctx.Request.RequestURI, "/assets/custom.css") {
|
||||||
serveFileOrEmptyContent(w, r, server.Config.Server.CustomCSSFile)
|
serveFileOrEmptyContent(ctx.Writer, ctx.Request, server.Config.Server.CustomCSSFile)
|
||||||
} else {
|
} else {
|
||||||
assetHandler.ServeHTTP(w, r)
|
serveFile(fs)(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveFile(f *prefixFS) func(ctx *gin.Context) {
|
||||||
|
return func(ctx *gin.Context) {
|
||||||
|
file, err := f.Open(ctx.Request.URL.Path)
|
||||||
|
if err != nil {
|
||||||
|
code := http.StatusInternalServerError
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
code = http.StatusNotFound
|
||||||
|
} else if errors.Is(err, fs.ErrPermission) {
|
||||||
|
code = http.StatusForbidden
|
||||||
|
}
|
||||||
|
ctx.Status(code)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Status(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var mime string
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(ctx.Request.URL.Path, ".js"):
|
||||||
|
mime = "text/javascript"
|
||||||
|
case strings.HasSuffix(ctx.Request.URL.Path, ".css"):
|
||||||
|
mime = "text/css"
|
||||||
|
case strings.HasSuffix(ctx.Request.URL.Path, ".png"):
|
||||||
|
mime = "image/png"
|
||||||
|
case strings.HasSuffix(ctx.Request.URL.Path, ".svg"):
|
||||||
|
mime = "image/svg"
|
||||||
|
}
|
||||||
|
ctx.Status(http.StatusOK)
|
||||||
|
ctx.Writer.Header().Set("Content-Type", mime)
|
||||||
|
if _, err := ctx.Writer.Write(replaceBytes(data)); err != nil {
|
||||||
|
log.Error().Err(err).Msgf("can not write %s", ctx.Request.URL.Path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,15 +149,24 @@ func handleIndex(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadFile(path string) ([]byte, error) {
|
||||||
|
data, err := web.Lookup(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return replaceBytes(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceBytes(data []byte) []byte {
|
||||||
|
return bytes.ReplaceAll(data, []byte("/BASE_PATH"), []byte(server.Config.Server.RootPath))
|
||||||
|
}
|
||||||
|
|
||||||
func parseIndex() []byte {
|
func parseIndex() []byte {
|
||||||
data, err := web.Lookup("index.html")
|
data, err := loadFile("index.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("can not find index.html")
|
log.Fatal().Err(err).Msg("can not find index.html")
|
||||||
}
|
}
|
||||||
if server.Config.Server.RootURL == "" {
|
return data
|
||||||
return data
|
|
||||||
}
|
|
||||||
return regexp.MustCompile(`/\S+\.(js|css|png|svg)`).ReplaceAll(data, []byte(server.Config.Server.RootURL+"$0"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupCache(c *gin.Context) {
|
func setupCache(c *gin.Context) {
|
||||||
|
|
|
@ -7,12 +7,12 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="theme-color" content="#65a30d" />
|
<meta name="theme-color" content="#65a30d" />
|
||||||
<title>Woodpecker</title>
|
<title>Woodpecker</title>
|
||||||
<link rel="stylesheet" href="/assets/custom.css" />
|
<script type="" src="/BASE_PATH/web-config.js"></script>
|
||||||
<script type="" src="/web-config.js"></script>
|
<link rel="stylesheet" href="/BASE_PATH/assets/custom.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
<script type="application/javascript" src="/assets/custom.js"></script>
|
<script type="application/javascript" src="/BASE_PATH/assets/custom.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build --base=/BASE_PATH",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"lint": "eslint --max-warnings 0 --ext .js,.ts,.vue,.json .",
|
"lint": "eslint --max-warnings 0 --ext .js,.ts,.vue,.json .",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="white"><path d="M1.263 2.744C2.41 3.832 2.845 4.932 4.118 5.08l.036.007c-.588.606-1.09 1.402-1.443 2.423-.38 1.096-.488 2.285-.614 3.659-.19 2.046-.401 4.364-1.556 7.269-2.486 6.258-1.12 11.63.332 17.317.664 2.604 1.348 5.297 1.642 8.107a.857.857 0 00.633.744.86.86 0 00.922-.323c.227-.313.524-.797.86-1.424.84 3.323 1.355 6.13 1.783 8.697a.866.866 0 001.517.41c2.88-3.463 3.763-8.636 2.184-12.674.459-2.433 1.402-4.45 2.398-6.583.536-1.15 1.08-2.318 1.55-3.566.228-.084.569-.314.79-.441l1.707-.981-.256 1.052a.864.864 0 001.678.408l.68-2.858 1.285-2.95a.863.863 0 10-1.581-.687l-1.152 2.669-2.383 1.372a18.97 18.97 0 00.508-2.981c.432-4.86-.718-9.074-3.066-11.266-.163-.157-.208-.281-.247-.26.095-.12.249-.26.358-.374 2.283-1.693 6.047-.147 8.319.75.589.232.876-.337.316-.67-1.95-1.153-5.948-4.196-8.188-6.193-.313-.275-.527-.607-.89-.913C9.825.555 4.072 3.057 1.355 2.569c-.102-.018-.166.103-.092.175m10.98 5.899c-.06 1.242-.603 1.8-1 2.208-.217.224-.426.436-.524.738-.236.714.008 1.51.66 2.143 1.974 1.84 2.925 5.527 2.538 9.86-.291 3.288-1.448 5.763-2.671 8.385-1.031 2.207-2.096 4.489-2.577 7.259a.853.853 0 00.056.48c1.02 2.434 1.135 6.197-.672 9.46a96.586 96.586 0 00-1.97-8.711c1.964-4.488 4.203-11.75 2.919-17.668-.325-1.497-1.304-3.276-2.387-4.207-.208-.18-.402-.237-.495-.167-.084.06-.151.238-.062.444.55 1.266.879 2.599 1.226 4.276 1.125 5.443-.956 12.49-2.835 16.782l-.116.259-.457.982c-.356-2.014-.85-3.95-1.33-5.84-1.38-5.406-2.68-10.515-.401-16.254 1.247-3.137 1.483-5.692 1.672-7.746.116-1.263.216-2.355.526-3.252.905-2.605 3.062-3.178 4.744-2.852 1.632.316 3.24 1.593 3.156 3.42zm-2.868.62a1.177 1.177 0 10.736-2.236 1.178 1.178 0 10-.736 2.237z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" fill="white"><path d="M1.263 2.744C2.41 3.832 2.845 4.932 4.118 5.08l.036.007c-.588.606-1.09 1.402-1.443 2.423-.38 1.096-.488 2.285-.614 3.659-.19 2.046-.401 4.364-1.556 7.269-2.486 6.258-1.12 11.63.332 17.317.664 2.604 1.348 5.297 1.642 8.107a.857.857 0 00.633.744.86.86 0 00.922-.323c.227-.313.524-.797.86-1.424.84 3.323 1.355 6.13 1.783 8.697a.866.866 0 001.517.41c2.88-3.463 3.763-8.636 2.184-12.674.459-2.433 1.402-4.45 2.398-6.583.536-1.15 1.08-2.318 1.55-3.566.228-.084.569-.314.79-.441l1.707-.981-.256 1.052a.864.864 0 001.678.408l.68-2.858 1.285-2.95a.863.863 0 10-1.581-.687l-1.152 2.669-2.383 1.372a18.97 18.97 0 00.508-2.981c.432-4.86-.718-9.074-3.066-11.266-.163-.157-.208-.281-.247-.26.095-.12.249-.26.358-.374 2.283-1.693 6.047-.147 8.319.75.589.232.876-.337.316-.67-1.95-1.153-5.948-4.196-8.188-6.193-.313-.275-.527-.607-.89-.913C9.825.555 4.072 3.057 1.355 2.569c-.102-.018-.166.103-.092.175m10.98 5.899c-.06 1.242-.603 1.8-1 2.208-.217.224-.426.436-.524.738-.236.714.008 1.51.66 2.143 1.974 1.84 2.925 5.527 2.538 9.86-.291 3.288-1.448 5.763-2.671 8.385-1.031 2.207-2.096 4.489-2.577 7.259a.853.853 0 00.056.48c1.02 2.434 1.135 6.197-.672 9.46a96.586 96.586 0 00-1.97-8.711c1.964-4.488 4.203-11.75 2.919-17.668-.325-1.497-1.304-3.276-2.387-4.207-.208-.18-.402-.237-.495-.167-.084.06-.151.238-.062.444.55 1.266.879 2.599 1.226 4.276 1.125 5.443-.956 12.49-2.835 16.782l-.116.259-.457.982c-.356-2.014-.85-3.95-1.33-5.84-1.38-5.406-2.68-10.515-.401-16.254 1.247-3.137 1.483-5.692 1.672-7.746.116-1.263.216-2.355.526-3.252.905-2.605 3.062-3.178 4.744-2.852 1.632.316 3.24 1.593 3.156 3.42zm-2.868.62a1.177 1.177 0 10.736-2.236 1.178 1.178 0 10-.736 2.237z"/></svg>
|
||||||
|
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
@ -7,7 +7,7 @@
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<router-link :to="{ name: 'home' }" class="flex flex-col -my-2 px-2">
|
<router-link :to="{ name: 'home' }" class="flex flex-col -my-2 px-2">
|
||||||
<img class="w-8 h-8" src="../../../assets/logo.svg?url" />
|
<WoodpeckerLogo class="w-8 h-8" />
|
||||||
<span class="text-xs">{{ version }}</span>
|
<span class="text-xs">{{ version }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<!-- Repo Link -->
|
<!-- Repo Link -->
|
||||||
|
@ -57,6 +57,7 @@
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
import WoodpeckerLogo from '~/assets/logo.svg?component';
|
||||||
import Button from '~/components/atomic/Button.vue';
|
import Button from '~/components/atomic/Button.vue';
|
||||||
import IconButton from '~/components/atomic/IconButton.vue';
|
import IconButton from '~/components/atomic/IconButton.vue';
|
||||||
import useAuthentication from '~/compositions/useAuthentication';
|
import useAuthentication from '~/compositions/useAuthentication';
|
||||||
|
@ -68,7 +69,7 @@ import ActivePipelines from './ActivePipelines.vue';
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'Navbar',
|
name: 'Navbar',
|
||||||
|
|
||||||
components: { Button, ActivePipelines, IconButton },
|
components: { Button, ActivePipelines, IconButton, WoodpeckerLogo },
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
|
@ -76,7 +77,7 @@ export default defineComponent({
|
||||||
const authentication = useAuthentication();
|
const authentication = useAuthentication();
|
||||||
const { darkMode } = useDarkMode();
|
const { darkMode } = useDarkMode();
|
||||||
const docsUrl = config.docs || undefined;
|
const docsUrl = config.docs || undefined;
|
||||||
const apiUrl = `${config.rootURL ?? ''}/swagger/index.html`;
|
const apiUrl = `${config.rootPath ?? ''}/swagger/index.html`;
|
||||||
|
|
||||||
function doLogin() {
|
function doLogin() {
|
||||||
authentication.authenticate(route.fullPath);
|
authentication.authenticate(route.fullPath);
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import WoodpeckerIcon from '../../../assets/woodpecker.svg?component';
|
import WoodpeckerIcon from '~/assets/woodpecker.svg?component';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -48,6 +48,7 @@ import InputField from '~/components/form/InputField.vue';
|
||||||
import SelectField from '~/components/form/SelectField.vue';
|
import SelectField from '~/components/form/SelectField.vue';
|
||||||
import Panel from '~/components/layout/Panel.vue';
|
import Panel from '~/components/layout/Panel.vue';
|
||||||
import useApiClient from '~/compositions/useApiClient';
|
import useApiClient from '~/compositions/useApiClient';
|
||||||
|
import useConfig from '~/compositions/useConfig';
|
||||||
import { usePaginate } from '~/compositions/usePaginate';
|
import { usePaginate } from '~/compositions/usePaginate';
|
||||||
import { Repo } from '~/lib/api/types';
|
import { Repo } from '~/lib/api/types';
|
||||||
|
|
||||||
|
@ -89,7 +90,7 @@ export default defineComponent({
|
||||||
|
|
||||||
const baseUrl = `${window.location.protocol}//${window.location.hostname}${
|
const baseUrl = `${window.location.protocol}//${window.location.hostname}${
|
||||||
window.location.port ? `:${window.location.port}` : ''
|
window.location.port ? `:${window.location.port}` : ''
|
||||||
}`;
|
}${useConfig().rootPath}`;
|
||||||
const badgeUrl = computed(
|
const badgeUrl = computed(
|
||||||
() => `/api/badges/${repo.value.id}/status.svg${branch.value !== '' ? `?branch=${branch.value}` : ''}`,
|
() => `/api/badges/${repo.value.id}/status.svg${branch.value !== '' ? `?branch=${branch.value}` : ''}`,
|
||||||
);
|
);
|
||||||
|
|
|
@ -63,7 +63,7 @@ import useApiClient from '~/compositions/useApiClient';
|
||||||
import useConfig from '~/compositions/useConfig';
|
import useConfig from '~/compositions/useConfig';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { enableSwagger } = useConfig();
|
const { rootPath, enableSwagger } = useConfig();
|
||||||
|
|
||||||
const apiClient = useApiClient();
|
const apiClient = useApiClient();
|
||||||
const token = ref<string | undefined>();
|
const token = ref<string | undefined>();
|
||||||
|
@ -72,7 +72,7 @@ onMounted(async () => {
|
||||||
token.value = await apiClient.getToken();
|
token.value = await apiClient.getToken();
|
||||||
});
|
});
|
||||||
|
|
||||||
const address = `${window.location.protocol}//${window.location.host}`; // port is included in location.host
|
const address = `${window.location.protocol}//${window.location.host}${rootPath}`; // port is included in location.host
|
||||||
|
|
||||||
const usageWithShell = computed(() => {
|
const usageWithShell = computed(() => {
|
||||||
let usage = `export WOODPECKER_SERVER="${address}"\n`;
|
let usage = `export WOODPECKER_SERVER="${address}"\n`;
|
||||||
|
|
|
@ -7,7 +7,7 @@ let apiClient: WoodpeckerClient | undefined;
|
||||||
export default (): WoodpeckerClient => {
|
export default (): WoodpeckerClient => {
|
||||||
if (!apiClient) {
|
if (!apiClient) {
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const server = config.rootURL ?? '';
|
const server = config.rootPath;
|
||||||
const token = null;
|
const token = null;
|
||||||
const csrf = config.csrf || null;
|
const csrf = config.csrf || null;
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,6 @@ export default () =>
|
||||||
const config = useUserConfig();
|
const config = useUserConfig();
|
||||||
config.setUserConfig('redirectUrl', url);
|
config.setUserConfig('redirectUrl', url);
|
||||||
}
|
}
|
||||||
window.location.href = '/login';
|
window.location.href = `${useConfig().rootPath}/login`;
|
||||||
},
|
},
|
||||||
} as const);
|
} as const);
|
||||||
|
|
|
@ -7,7 +7,7 @@ declare global {
|
||||||
WOODPECKER_VERSION: string | undefined;
|
WOODPECKER_VERSION: string | undefined;
|
||||||
WOODPECKER_CSRF: string | undefined;
|
WOODPECKER_CSRF: string | undefined;
|
||||||
WOODPECKER_FORGE: string | undefined;
|
WOODPECKER_FORGE: string | undefined;
|
||||||
WOODPECKER_ROOT_URL: string | undefined;
|
WOODPECKER_ROOT_PATH: string | undefined;
|
||||||
WOODPECKER_ENABLE_SWAGGER: boolean | undefined;
|
WOODPECKER_ENABLE_SWAGGER: boolean | undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,6 @@ export default () => ({
|
||||||
version: window.WOODPECKER_VERSION,
|
version: window.WOODPECKER_VERSION,
|
||||||
csrf: window.WOODPECKER_CSRF || null,
|
csrf: window.WOODPECKER_CSRF || null,
|
||||||
forge: window.WOODPECKER_FORGE || null,
|
forge: window.WOODPECKER_FORGE || null,
|
||||||
rootURL: window.WOODPECKER_ROOT_URL || null,
|
rootPath: window.WOODPECKER_ROOT_PATH || '',
|
||||||
enableSwagger: window.WOODPECKER_ENABLE_SWAGGER || false,
|
enableSwagger: window.WOODPECKER_ENABLE_SWAGGER || false,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import useConfig from '~/compositions/useConfig';
|
||||||
import { useDarkMode } from '~/compositions/useDarkMode';
|
import { useDarkMode } from '~/compositions/useDarkMode';
|
||||||
import { PipelineStatus } from '~/lib/api/types';
|
import { PipelineStatus } from '~/lib/api/types';
|
||||||
|
|
||||||
|
@ -13,12 +14,16 @@ watch(
|
||||||
() => {
|
() => {
|
||||||
const faviconPNG = document.getElementById('favicon-png');
|
const faviconPNG = document.getElementById('favicon-png');
|
||||||
if (faviconPNG) {
|
if (faviconPNG) {
|
||||||
(faviconPNG as HTMLLinkElement).href = `/favicons/favicon-${darkMode.value}-${faviconStatus.value}.png`;
|
(faviconPNG as HTMLLinkElement).href = `${useConfig().rootPath}/favicons/favicon-${darkMode.value}-${
|
||||||
|
faviconStatus.value
|
||||||
|
}.png`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const faviconSVG = document.getElementById('favicon-svg');
|
const faviconSVG = document.getElementById('favicon-svg');
|
||||||
if (faviconSVG) {
|
if (faviconSVG) {
|
||||||
(faviconSVG as HTMLLinkElement).href = `/favicons/favicon-${darkMode.value}-${faviconStatus.value}.svg`;
|
(faviconSVG as HTMLLinkElement).href = `${useConfig().rootPath}/favicons/favicon-${darkMode.value}-${
|
||||||
|
faviconStatus.value
|
||||||
|
}.svg`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
|
|
|
@ -109,7 +109,7 @@ export default class ApiClient {
|
||||||
access_token: this.token || undefined,
|
access_token: this.token || undefined,
|
||||||
});
|
});
|
||||||
let _path = this.server ? this.server + path : path;
|
let _path = this.server ? this.server + path : path;
|
||||||
_path = this.token ? `${path}?${query}` : path;
|
_path = this.token ? `${_path}?${query}` : _path;
|
||||||
|
|
||||||
const events = new EventSource(_path);
|
const events = new EventSource(_path);
|
||||||
events.onmessage = (event) => {
|
events.onmessage = (event) => {
|
||||||
|
|
|
@ -2,16 +2,18 @@ import { Component } from 'vue';
|
||||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
|
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
import useAuthentication from '~/compositions/useAuthentication';
|
import useAuthentication from '~/compositions/useAuthentication';
|
||||||
|
import useConfig from '~/compositions/useConfig';
|
||||||
import useUserConfig from '~/compositions/useUserConfig';
|
import useUserConfig from '~/compositions/useUserConfig';
|
||||||
|
|
||||||
|
const { rootPath } = useConfig();
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: `${rootPath}/`,
|
||||||
name: 'home',
|
name: 'home',
|
||||||
redirect: '/repos',
|
redirect: `${rootPath}/repos`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/repos',
|
path: `${rootPath}/repos`,
|
||||||
component: (): Component => import('~/views/RouterView.vue'),
|
component: (): Component => import('~/views/RouterView.vue'),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
@ -105,7 +107,7 @@ const routes: RouteRecordRaw[] = [
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/orgs/:orgId',
|
path: `${rootPath}/orgs/:orgId`,
|
||||||
component: (): Component => import('~/views/org/OrgWrapper.vue'),
|
component: (): Component => import('~/views/org/OrgWrapper.vue'),
|
||||||
props: true,
|
props: true,
|
||||||
children: [
|
children: [
|
||||||
|
@ -125,12 +127,12 @@ const routes: RouteRecordRaw[] = [
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/org/:orgName/:pathMatch(.*)*',
|
path: `${rootPath}/org/:orgName/:pathMatch(.*)*`,
|
||||||
component: (): Component => import('~/views/org/OrgDeprecatedRedirect.vue'),
|
component: (): Component => import('~/views/org/OrgDeprecatedRedirect.vue'),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin',
|
path: `${rootPath}/admin`,
|
||||||
name: 'admin-settings',
|
name: 'admin-settings',
|
||||||
component: (): Component => import('~/views/admin/AdminSettings.vue'),
|
component: (): Component => import('~/views/admin/AdminSettings.vue'),
|
||||||
props: true,
|
props: true,
|
||||||
|
@ -138,21 +140,21 @@ const routes: RouteRecordRaw[] = [
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/user',
|
path: `${rootPath}/user`,
|
||||||
name: 'user',
|
name: 'user',
|
||||||
component: (): Component => import('~/views/User.vue'),
|
component: (): Component => import('~/views/User.vue'),
|
||||||
meta: { authentication: 'required' },
|
meta: { authentication: 'required' },
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/login/error',
|
path: `${rootPath}/login/error`,
|
||||||
name: 'login-error',
|
name: 'login-error',
|
||||||
component: (): Component => import('~/views/Login.vue'),
|
component: (): Component => import('~/views/Login.vue'),
|
||||||
meta: { blank: true },
|
meta: { blank: true },
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/do-login',
|
path: `${rootPath}/do-login`,
|
||||||
name: 'login',
|
name: 'login',
|
||||||
component: (): Component => import('~/views/Login.vue'),
|
component: (): Component => import('~/views/Login.vue'),
|
||||||
meta: { blank: true },
|
meta: { blank: true },
|
||||||
|
@ -161,18 +163,18 @@ const routes: RouteRecordRaw[] = [
|
||||||
|
|
||||||
// TODO: deprecated routes => remove after some time
|
// TODO: deprecated routes => remove after some time
|
||||||
{
|
{
|
||||||
path: '/:ownerOrOrgId',
|
path: `${rootPath}/:ownerOrOrgId`,
|
||||||
redirect: (route) => ({ name: 'org', params: route.params }),
|
redirect: (route) => ({ name: 'org', params: route.params }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/:repoOwner/:repoName/:pathMatch(.*)*',
|
path: `${rootPath}/:repoOwner/:repoName/:pathMatch(.*)*`,
|
||||||
component: () => import('~/views/repo/RepoDeprecatedRedirect.vue'),
|
component: () => import('~/views/repo/RepoDeprecatedRedirect.vue'),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// not found handler
|
// not found handler
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: `${rootPath}/:pathMatch(.*)*`,
|
||||||
name: 'not-found',
|
name: 'not-found',
|
||||||
component: (): Component => import('~/views/NotFound.vue'),
|
component: (): Component => import('~/views/NotFound.vue'),
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
class="flex flex-col w-full overflow-hidden md:m-8 md:rounded-md md:shadow md:border md:border-wp-background-400 md:bg-wp-background-100 md:dark:bg-wp-background-200 md:flex-row md:w-3xl md:h-sm justify-center"
|
class="flex flex-col w-full overflow-hidden md:m-8 md:rounded-md md:shadow md:border md:border-wp-background-400 md:bg-wp-background-100 md:dark:bg-wp-background-200 md:flex-row md:w-3xl md:h-sm justify-center"
|
||||||
>
|
>
|
||||||
<div class="flex md:bg-wp-primary-200 md:dark:bg-wp-primary-300 md:w-3/5 justify-center items-center">
|
<div class="flex md:bg-wp-primary-200 md:dark:bg-wp-primary-300 md:w-3/5 justify-center items-center">
|
||||||
<img class="w-48 h-48" src="../assets/logo.svg?url" />
|
<WoodpeckerLogo class="w-48 h-48" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col my-8 md:w-2/5 p-4 items-center justify-center">
|
<div class="flex flex-col my-8 md:w-2/5 p-4 items-center justify-center">
|
||||||
<h1 class="text-xl text-wp-text-100">{{ $t('welcome') }}</h1>
|
<h1 class="text-xl text-wp-text-100">{{ $t('welcome') }}</h1>
|
||||||
|
@ -27,6 +27,7 @@ import { defineComponent, onMounted, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import WoodpeckerLogo from '~/assets/logo.svg?component';
|
||||||
import Button from '~/components/atomic/Button.vue';
|
import Button from '~/components/atomic/Button.vue';
|
||||||
import useAuthentication from '~/compositions/useAuthentication';
|
import useAuthentication from '~/compositions/useAuthentication';
|
||||||
|
|
||||||
|
@ -35,6 +36,7 @@ export default defineComponent({
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
Button,
|
Button,
|
||||||
|
WoodpeckerLogo,
|
||||||
},
|
},
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
|
||||||
import Tab from '~/components/layout/scaffold/Tab.vue';
|
import Tab from '~/components/layout/scaffold/Tab.vue';
|
||||||
import UserAPITab from '~/components/user/UserAPITab.vue';
|
import UserAPITab from '~/components/user/UserAPITab.vue';
|
||||||
import UserGeneralTab from '~/components/user/UserGeneralTab.vue';
|
import UserGeneralTab from '~/components/user/UserGeneralTab.vue';
|
||||||
|
import useConfig from '~/compositions/useConfig';
|
||||||
|
|
||||||
const address = `${window.location.protocol}//${window.location.host}`; // port is included in location.host
|
const address = `${window.location.protocol}//${window.location.host}${useConfig().rootPath}`; // port is included in location.host
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -123,7 +123,7 @@ watch([repositoryId], () => {
|
||||||
loadRepo();
|
loadRepo();
|
||||||
});
|
});
|
||||||
|
|
||||||
const badgeUrl = computed(() => repo.value && `/api/badges/${repo.value.id}/status.svg`);
|
const badgeUrl = computed(() => repo.value && `${config.rootPath}/api/badges/${repo.value.id}/status.svg`);
|
||||||
|
|
||||||
const activeTab = computed({
|
const activeTab = computed({
|
||||||
get() {
|
get() {
|
||||||
|
|
Loading…
Reference in a new issue