Fix UI and backend paths with subpath ()

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  
Closes 
This commit is contained in:
qwerty287 2023-08-07 16:05:18 +02:00 committed by GitHub
parent 10b1cfcd3b
commit 67b7de5cc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 162 additions and 101 deletions

View File

@ -61,8 +61,8 @@ var flags = []cli.Flag{
Usage: "server fully qualified url for forge's Webhooks (<scheme>://<host>)",
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_ROOT_URL"},
Name: "root-url",
EnvVars: []string{"WOODPECKER_ROOT_PATH", "WOODPECKER_ROOT_URL"},
Name: "root-path",
Usage: "server url root (used for statics loading when having a url path prefix)",
},
&cli.StringFlag{

View File

@ -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.StatusContextFormat = c.String("status-context-format")
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.CustomJsFile = strings.TrimSpace(c.String("custom-js-file"))
server.Config.Pipeline.Networks = c.StringSlice("network")

View File

@ -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.
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).

View File

@ -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
### `WOODPECKER_ROOT_URL`
### `WOODPECKER_ROOT_PATH`
> Default: ``
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`
> Default: true

View File

@ -34,14 +34,10 @@ import (
)
func HandleLogin(c *gin.Context) {
var (
w = c.Writer
r = c.Request
)
if err := r.FormValue("error"); err != "" {
http.Redirect(w, r, "/login/error?code="+err, 303)
if err := c.Request.FormValue("error"); err != "" {
c.Redirect(http.StatusSeeOther, server.Config.Server.RootPath+"/login/error?code="+err)
} 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)
if err != nil {
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
}
// 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 !config.Open && !config.IsAdmin(tmpuser) {
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
}
@ -87,7 +83,7 @@ func HandleAuth(c *gin.Context) {
teams, terr := _forge.Teams(c, tmpuser)
if terr != nil || !config.IsMember(teams) {
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
}
}
@ -108,7 +104,7 @@ func HandleAuth(c *gin.Context) {
// insert the user into the database
if err := _store.CreateUser(u); err != nil {
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
}
@ -137,14 +133,14 @@ func HandleAuth(c *gin.Context) {
teams, terr := _forge.Teams(c, u)
if terr != nil || !config.IsMember(teams) {
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
}
}
if err := _store.UpdateUser(u); err != nil {
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
}
@ -152,7 +148,7 @@ func HandleAuth(c *gin.Context) {
tokenString, err := token.New(token.SessToken, u.Login).SignExpires(u.Hash, exp)
if err != nil {
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
}
@ -187,13 +183,13 @@ func HandleAuth(c *gin.Context) {
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) {
httputil.DelCookie(c.Writer, c.Request, "user_sess")
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) {

View File

@ -67,7 +67,7 @@ var Config = struct {
StatusContext string
StatusContextFormat string
SessionExpires time.Duration
RootURL string
RootPath string
CustomCSSFile string
CustomJsFile string
Migrations struct {

View File

@ -421,7 +421,7 @@ func (c *config) newOAuth2Config() *oauth2.Config {
AuthURL: fmt.Sprintf("%s/site/oauth2/authorize", 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),
}
}

View File

@ -103,7 +103,7 @@ func (c *Gitea) oauth2Config(ctx context.Context) (*oauth2.Config, context.Conte
AuthURL: fmt.Sprintf(authorizeTokenURL, 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{

View File

@ -395,9 +395,9 @@ func (c *client) newConfig(req *http.Request) *oauth2.Config {
intendedURL := req.URL.Query()["url"]
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 {
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{

View File

@ -93,7 +93,7 @@ func (g *GitLab) oauth2Config(ctx context.Context) (*oauth2.Config, context.Cont
TokenURL: fmt.Sprintf("%s/oauth/token", g.url),
},
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{

View File

@ -23,7 +23,7 @@ import (
"github.com/woodpecker-ci/woodpecker/server/router/middleware/session"
)
func apiRoutes(e *gin.Engine) {
func apiRoutes(e *gin.RouterGroup) {
apiBase := e.Group("/api")
{
user := apiBase.Group("/user")

View File

@ -22,9 +22,9 @@ import (
"github.com/rs/zerolog/log"
swaggerfiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/woodpecker-ci/woodpecker/cmd/server/docs"
"github.com/woodpecker-ci/woodpecker/server"
"github.com/woodpecker-ci/woodpecker/server/api"
"github.com/woodpecker-ci/woodpecker/server/api/metrics"
"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.GET("/web-config.js", web.Config)
e.GET("/logout", api.GetLogout)
e.GET("/login", api.HandleLogin)
auth := e.Group("/authorize")
base := e.Group(server.Config.Server.RootPath)
{
auth.GET("", api.HandleAuth)
auth.POST("", api.HandleAuth)
auth.POST("/token", api.GetLoginToken)
base.GET("/web-config.js", web.Config)
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())
e.GET("/version", api.Version)
e.GET("/healthz", api.Health)
apiRoutes(e)
apiRoutes(base)
if server.Config.Server.EnableSwagger {
setupSwaggerConfigAndRoutes(e)
}
@ -78,8 +81,8 @@ func Load(noRouteHandler http.HandlerFunc, middleware ...gin.HandlerFunc) http.H
func setupSwaggerConfigAndRoutes(e *gin.Engine) {
docs.SwaggerInfo.Host = getHost(server.Config.Server.Host)
docs.SwaggerInfo.BasePath = server.Config.Server.RootURL + "/api"
e.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
docs.SwaggerInfo.BasePath = server.Config.Server.RootPath + "/api"
e.GET(server.Config.Server.RootPath+"/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
}
func getHost(s string) string {

View File

@ -45,7 +45,7 @@ func Config(c *gin.Context) {
"docs": server.Config.Server.Docs,
"version": version.String(),
"forge": server.Config.Services.Forge.Name(),
"root_url": server.Config.Server.RootURL,
"root_path": server.Config.Server.RootPath,
"enable_swagger": server.Config.Server.EnableSwagger,
}
@ -75,6 +75,6 @@ window.WOODPECKER_CSRF = "{{ .csrf }}";
window.WOODPECKER_VERSION = "{{ .version }}";
window.WOODPECKER_DOCS = "{{ .docs }}";
window.WOODPECKER_FORGE = "{{ .forge }}";
window.WOODPECKER_ROOT_URL = "{{ .root_url }}";
window.WOODPECKER_ROOT_PATH = "{{ .root_path }}";
window.WOODPECKER_ENABLE_SWAGGER = {{ .enable_swagger }};
`

View File

@ -17,10 +17,11 @@ package web
import (
"bytes"
"crypto/md5"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"net/url"
"regexp"
"strings"
"time"
@ -54,24 +55,23 @@ func New() (*gin.Engine, error) {
e.Use(setupCache)
rootURL, _ := url.Parse(server.Config.Server.RootURL)
rootPath := rootURL.Path
rootPath := server.Config.Server.RootPath
httpFS, err := web.HTTPFS()
if err != nil {
return nil, err
}
h := http.FileServer(&prefixFS{httpFS, rootPath})
e.GET(rootPath+"/favicon.svg", redirect(server.Config.Server.RootURL+"/favicons/favicon-light-default.svg", http.StatusPermanentRedirect))
e.GET(rootPath+"/favicons/*filepath", gin.WrapH(h))
e.GET(rootPath+"/assets/*filepath", gin.WrapH(handleCustomFilesAndAssets(h)))
f := &prefixFS{httpFS, rootPath}
e.GET(rootPath+"/favicon.svg", redirect(server.Config.Server.RootPath+"/favicons/favicon-light-default.svg", http.StatusPermanentRedirect))
e.GET(rootPath+"/favicons/*filepath", serveFile(f))
e.GET(rootPath+"/assets/*filepath", handleCustomFilesAndAssets(f))
e.NoRoute(handleIndex)
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) {
if len(localFileName) > 0 {
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{}))
}
}
return func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.RequestURI, "/assets/custom.js") {
serveFileOrEmptyContent(w, r, server.Config.Server.CustomJsFile)
} else if strings.HasSuffix(r.RequestURI, "/assets/custom.css") {
serveFileOrEmptyContent(w, r, server.Config.Server.CustomCSSFile)
return func(ctx *gin.Context) {
if strings.HasSuffix(ctx.Request.RequestURI, "/assets/custom.js") {
serveFileOrEmptyContent(ctx.Writer, ctx.Request, server.Config.Server.CustomJsFile)
} else if strings.HasSuffix(ctx.Request.RequestURI, "/assets/custom.css") {
serveFileOrEmptyContent(ctx.Writer, ctx.Request, server.Config.Server.CustomCSSFile)
} 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 {
data, err := web.Lookup("index.html")
data, err := loadFile("index.html")
if err != nil {
log.Fatal().Err(err).Msg("can not find index.html")
}
if server.Config.Server.RootURL == "" {
return data
}
return regexp.MustCompile(`/\S+\.(js|css|png|svg)`).ReplaceAll(data, []byte(server.Config.Server.RootURL+"$0"))
return data
}
func setupCache(c *gin.Context) {

View File

@ -7,12 +7,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#65a30d" />
<title>Woodpecker</title>
<link rel="stylesheet" href="/assets/custom.css" />
<script type="" src="/web-config.js"></script>
<script type="" src="/BASE_PATH/web-config.js"></script>
<link rel="stylesheet" href="/BASE_PATH/assets/custom.css" />
</head>
<body>
<div id="app"></div>
<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>
</html>

View File

@ -8,7 +8,7 @@
},
"scripts": {
"start": "vite",
"build": "vite build",
"build": "vite build --base=/BASE_PATH",
"serve": "vite preview",
"lint": "eslint --max-warnings 0 --ext .js,.ts,.vue,.json .",
"format": "prettier --write .",

View File

@ -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

(image error) Size: 1.7 KiB

After

(image error) Size: 1.7 KiB

View File

@ -7,7 +7,7 @@
<div class="flex items-center space-x-2">
<!-- Logo -->
<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>
</router-link>
<!-- Repo Link -->
@ -57,6 +57,7 @@
import { defineComponent } from 'vue';
import { useRoute } from 'vue-router';
import WoodpeckerLogo from '~/assets/logo.svg?component';
import Button from '~/components/atomic/Button.vue';
import IconButton from '~/components/atomic/IconButton.vue';
import useAuthentication from '~/compositions/useAuthentication';
@ -68,7 +69,7 @@ import ActivePipelines from './ActivePipelines.vue';
export default defineComponent({
name: 'Navbar',
components: { Button, ActivePipelines, IconButton },
components: { Button, ActivePipelines, IconButton, WoodpeckerLogo },
setup() {
const config = useConfig();
@ -76,7 +77,7 @@ export default defineComponent({
const authentication = useAuthentication();
const { darkMode } = useDarkMode();
const docsUrl = config.docs || undefined;
const apiUrl = `${config.rootURL ?? ''}/swagger/index.html`;
const apiUrl = `${config.rootPath ?? ''}/swagger/index.html`;
function doLogin() {
authentication.authenticate(route.fullPath);

View File

@ -3,7 +3,7 @@
</template>
<script lang="ts" setup>
import WoodpeckerIcon from '../../../assets/woodpecker.svg?component';
import WoodpeckerIcon from '~/assets/woodpecker.svg?component';
</script>
<style scoped>

View File

@ -48,6 +48,7 @@ import InputField from '~/components/form/InputField.vue';
import SelectField from '~/components/form/SelectField.vue';
import Panel from '~/components/layout/Panel.vue';
import useApiClient from '~/compositions/useApiClient';
import useConfig from '~/compositions/useConfig';
import { usePaginate } from '~/compositions/usePaginate';
import { Repo } from '~/lib/api/types';
@ -89,7 +90,7 @@ export default defineComponent({
const baseUrl = `${window.location.protocol}//${window.location.hostname}${
window.location.port ? `:${window.location.port}` : ''
}`;
}${useConfig().rootPath}`;
const badgeUrl = computed(
() => `/api/badges/${repo.value.id}/status.svg${branch.value !== '' ? `?branch=${branch.value}` : ''}`,
);

View File

@ -63,7 +63,7 @@ import useApiClient from '~/compositions/useApiClient';
import useConfig from '~/compositions/useConfig';
const { t } = useI18n();
const { enableSwagger } = useConfig();
const { rootPath, enableSwagger } = useConfig();
const apiClient = useApiClient();
const token = ref<string | undefined>();
@ -72,7 +72,7 @@ onMounted(async () => {
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(() => {
let usage = `export WOODPECKER_SERVER="${address}"\n`;

View File

@ -7,7 +7,7 @@ let apiClient: WoodpeckerClient | undefined;
export default (): WoodpeckerClient => {
if (!apiClient) {
const config = useConfig();
const server = config.rootURL ?? '';
const server = config.rootPath;
const token = null;
const csrf = config.csrf || null;

View File

@ -12,6 +12,6 @@ export default () =>
const config = useUserConfig();
config.setUserConfig('redirectUrl', url);
}
window.location.href = '/login';
window.location.href = `${useConfig().rootPath}/login`;
},
} as const);

View File

@ -7,7 +7,7 @@ declare global {
WOODPECKER_VERSION: string | undefined;
WOODPECKER_CSRF: string | undefined;
WOODPECKER_FORGE: string | undefined;
WOODPECKER_ROOT_URL: string | undefined;
WOODPECKER_ROOT_PATH: string | undefined;
WOODPECKER_ENABLE_SWAGGER: boolean | undefined;
}
}
@ -18,6 +18,6 @@ export default () => ({
version: window.WOODPECKER_VERSION,
csrf: window.WOODPECKER_CSRF || null,
forge: window.WOODPECKER_FORGE || null,
rootURL: window.WOODPECKER_ROOT_URL || null,
rootPath: window.WOODPECKER_ROOT_PATH || '',
enableSwagger: window.WOODPECKER_ENABLE_SWAGGER || false,
});

View File

@ -1,5 +1,6 @@
import { computed, ref, watch } from 'vue';
import useConfig from '~/compositions/useConfig';
import { useDarkMode } from '~/compositions/useDarkMode';
import { PipelineStatus } from '~/lib/api/types';
@ -13,12 +14,16 @@ watch(
() => {
const faviconPNG = document.getElementById('favicon-png');
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');
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 },

View File

@ -109,7 +109,7 @@ export default class ApiClient {
access_token: this.token || undefined,
});
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);
events.onmessage = (event) => {

View File

@ -2,16 +2,18 @@ import { Component } from 'vue';
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import useAuthentication from '~/compositions/useAuthentication';
import useConfig from '~/compositions/useConfig';
import useUserConfig from '~/compositions/useUserConfig';
const { rootPath } = useConfig();
const routes: RouteRecordRaw[] = [
{
path: '/',
path: `${rootPath}/`,
name: 'home',
redirect: '/repos',
redirect: `${rootPath}/repos`,
},
{
path: '/repos',
path: `${rootPath}/repos`,
component: (): Component => import('~/views/RouterView.vue'),
children: [
{
@ -105,7 +107,7 @@ const routes: RouteRecordRaw[] = [
],
},
{
path: '/orgs/:orgId',
path: `${rootPath}/orgs/:orgId`,
component: (): Component => import('~/views/org/OrgWrapper.vue'),
props: true,
children: [
@ -125,12 +127,12 @@ const routes: RouteRecordRaw[] = [
],
},
{
path: '/org/:orgName/:pathMatch(.*)*',
path: `${rootPath}/org/:orgName/:pathMatch(.*)*`,
component: (): Component => import('~/views/org/OrgDeprecatedRedirect.vue'),
props: true,
},
{
path: '/admin',
path: `${rootPath}/admin`,
name: 'admin-settings',
component: (): Component => import('~/views/admin/AdminSettings.vue'),
props: true,
@ -138,21 +140,21 @@ const routes: RouteRecordRaw[] = [
},
{
path: '/user',
path: `${rootPath}/user`,
name: 'user',
component: (): Component => import('~/views/User.vue'),
meta: { authentication: 'required' },
props: true,
},
{
path: '/login/error',
path: `${rootPath}/login/error`,
name: 'login-error',
component: (): Component => import('~/views/Login.vue'),
meta: { blank: true },
props: true,
},
{
path: '/do-login',
path: `${rootPath}/do-login`,
name: 'login',
component: (): Component => import('~/views/Login.vue'),
meta: { blank: true },
@ -161,18 +163,18 @@ const routes: RouteRecordRaw[] = [
// TODO: deprecated routes => remove after some time
{
path: '/:ownerOrOrgId',
path: `${rootPath}/:ownerOrOrgId`,
redirect: (route) => ({ name: 'org', params: route.params }),
},
{
path: '/:repoOwner/:repoName/:pathMatch(.*)*',
path: `${rootPath}/:repoOwner/:repoName/:pathMatch(.*)*`,
component: () => import('~/views/repo/RepoDeprecatedRedirect.vue'),
props: true,
},
// not found handler
{
path: '/:pathMatch(.*)*',
path: `${rootPath}/:pathMatch(.*)*`,
name: 'not-found',
component: (): Component => import('~/views/NotFound.vue'),
},

View File

@ -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"
>
<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 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>
@ -27,6 +27,7 @@ import { defineComponent, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import WoodpeckerLogo from '~/assets/logo.svg?component';
import Button from '~/components/atomic/Button.vue';
import useAuthentication from '~/compositions/useAuthentication';
@ -35,6 +36,7 @@ export default defineComponent({
components: {
Button,
WoodpeckerLogo,
},
setup() {

View File

@ -16,6 +16,7 @@ import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import Tab from '~/components/layout/scaffold/Tab.vue';
import UserAPITab from '~/components/user/UserAPITab.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>

View File

@ -123,7 +123,7 @@ watch([repositoryId], () => {
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({
get() {