From 3f5f4e01f1a875b45c42215d0fa976bf829ad1d2 Mon Sep 17 00:00:00 2001 From: Thomas Miceli <27960254+thomiceli@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:12:54 +0200 Subject: [PATCH] Add custom static links (#234) --- config.yml | 10 ++++- docs/configuration/cheat-sheet.md | 69 +++++++++++++++-------------- docs/configuration/custom-assets.md | 31 +++++++++++++ docs/configuration/custom-links.md | 38 ++++++++++++++++ internal/config/config.go | 56 ++++++++++++++++++++--- internal/web/server.go | 48 +++++++++++++++----- templates/base/base_footer.html | 9 +++- 7 files changed, 209 insertions(+), 52 deletions(-) create mode 100644 docs/configuration/custom-assets.md create mode 100644 docs/configuration/custom-links.md diff --git a/config.yml b/config.yml index 94edcd0..604a6bb 100644 --- a/config.yml +++ b/config.yml @@ -97,6 +97,14 @@ oidc.discovery-url: # Custom assets -# Add your own custom assets to $opengist-home/custom/ +# Add your own custom assets, that are files relatives to $opengist-home/custom/ custom.logo: custom.favicon: + +# Static pages in footer (like legal notices, privacy policy, etc.) +# The path can be a URL or a relative path to a file in the $opengist-home/custom/ directory +custom.static-links: +# - name: Gitea +# path: https://gitea.com +# - name: Legal notices +# path: legal.html diff --git a/docs/configuration/cheat-sheet.md b/docs/configuration/cheat-sheet.md index 981af6d..ef7768f 100644 --- a/docs/configuration/cheat-sheet.md +++ b/docs/configuration/cheat-sheet.md @@ -1,36 +1,37 @@ # Configuration Cheat Sheet -| YAML Config Key | Environment Variable | Default value | Description | -|-----------------------|--------------------------|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `panic`. | -| log-output | OG_LOG_OUTPUT | `stdout,file` | Set the log output to one or more of the following: `stdout`, `file`. | -| external-url | OG_EXTERNAL_URL | none | Public URL to access to Opengist. | -| opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. | -| db-filename | OG_DB_FILENAME | `opengist.db` | Name of the SQLite database file. | -| index.enabled | OG_INDEX_ENABLED | `true` | Enable or disable the code search index (`true` or `false`) | -| index.dirname | OG_INDEX_DIRNAME | `opengist.index` | Name of the directory where the code search index is stored. | -| git.default-branch | OG_GIT_DEFAULT_BRANCH | none | Default branch name used by Opengist when initializing Git repositories. If not set, uses the Git default branch name. More info [here](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch) | -| sqlite.journal-mode | OG_SQLITE_JOURNAL_MODE | `WAL` | Set the journal mode for SQLite. More info [here](https://www.sqlite.org/pragma.html#pragma_journal_mode) | -| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. | -| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. | -| http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) | -| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) | -| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. | -| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. | -| ssh.external-domain | OG_SSH_EXTERNAL_DOMAIN | none | Public domain for the Git SSH connection, if it has to be different from the HTTP one. If not set, uses the URL from the request. | -| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. | -| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. | -| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. | -| gitlab.client-key | OG_GITLAB_CLIENT_KEY | none | The client key for the GitLab OAuth application. | -| gitlab.secret | OG_GITLAB_SECRET | none | The secret for the GitLab OAuth application. | -| gitlab.url | OG_GITLAB_URL | `https://gitlab.com/` | The URL of the GitLab instance. | -| gitlab.name | OG_GITLAB_NAME | `GitLab` | The name of the GitLab instance. It is displayed in the OAuth login button. | -| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. | -| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. | -| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. | -| gitea.name | OG_GITEA_NAME | `Gitea` | The name of the Gitea instance. It is displayed in the OAuth login button. | -| oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. | -| oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. | -| oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. | -| custom.logo | OG_CUSTOM_LOGO | none | Path to an image, relative to $opengist-home/custom | -| custom.favicon | OG_CUSTOM_FAVICON | none | Path to an image, relative to $opengist-home/custom | +| YAML Config Key | Environment Variable | Default value | Description | +|-----------------------|-------------------------------------|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| log-level | OG_LOG_LEVEL | `warn` | Set the log level to one of the following: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `panic`. | +| log-output | OG_LOG_OUTPUT | `stdout,file` | Set the log output to one or more of the following: `stdout`, `file`. | +| external-url | OG_EXTERNAL_URL | none | Public URL to access to Opengist. | +| opengist-home | OG_OPENGIST_HOME | home directory | Path to the directory where Opengist stores its data. | +| db-filename | OG_DB_FILENAME | `opengist.db` | Name of the SQLite database file. | +| index.enabled | OG_INDEX_ENABLED | `true` | Enable or disable the code search index (`true` or `false`) | +| index.dirname | OG_INDEX_DIRNAME | `opengist.index` | Name of the directory where the code search index is stored. | +| git.default-branch | OG_GIT_DEFAULT_BRANCH | none | Default branch name used by Opengist when initializing Git repositories. If not set, uses the Git default branch name. More info [here](https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup#_new_default_branch) | +| sqlite.journal-mode | OG_SQLITE_JOURNAL_MODE | `WAL` | Set the journal mode for SQLite. More info [here](https://www.sqlite.org/pragma.html#pragma_journal_mode) | +| http.host | OG_HTTP_HOST | `0.0.0.0` | The host on which the HTTP server should bind. | +| http.port | OG_HTTP_PORT | `6157` | The port on which the HTTP server should listen. | +| http.git-enabled | OG_HTTP_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via HTTP. (`true` or `false`) | +| ssh.git-enabled | OG_SSH_GIT_ENABLED | `true` | Enable or disable git operations (clone, pull, push) via SSH. (`true` or `false`) | +| ssh.host | OG_SSH_HOST | `0.0.0.0` | The host on which the SSH server should bind. | +| ssh.port | OG_SSH_PORT | `2222` | The port on which the SSH server should listen. | +| ssh.external-domain | OG_SSH_EXTERNAL_DOMAIN | none | Public domain for the Git SSH connection, if it has to be different from the HTTP one. If not set, uses the URL from the request. | +| ssh.keygen-executable | OG_SSH_KEYGEN_EXECUTABLE | `ssh-keygen` | Path to the SSH key generation executable. | +| github.client-key | OG_GITHUB_CLIENT_KEY | none | The client key for the GitHub OAuth application. | +| github.secret | OG_GITHUB_SECRET | none | The secret for the GitHub OAuth application. | +| gitlab.client-key | OG_GITLAB_CLIENT_KEY | none | The client key for the GitLab OAuth application. | +| gitlab.secret | OG_GITLAB_SECRET | none | The secret for the GitLab OAuth application. | +| gitlab.url | OG_GITLAB_URL | `https://gitlab.com/` | The URL of the GitLab instance. | +| gitlab.name | OG_GITLAB_NAME | `GitLab` | The name of the GitLab instance. It is displayed in the OAuth login button. | +| gitea.client-key | OG_GITEA_CLIENT_KEY | none | The client key for the Gitea OAuth application. | +| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. | +| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. | +| gitea.name | OG_GITEA_NAME | `Gitea` | The name of the Gitea instance. It is displayed in the OAuth login button. | +| oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. | +| oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. | +| oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. | +| custom.logo | OG_CUSTOM_LOGO | none | Path to an image, relative to $opengist-home/custom. | +| custom.favicon | OG_CUSTOM_FAVICON | none | Path to an image, relative to $opengist-home/custom. | +| custom.static-links | OG_CUSTOM_STATIC_LINK_#_(PATH,NAME) | none | Path and name to custom links, more info [here](custom-links.md). | diff --git a/docs/configuration/custom-assets.md b/docs/configuration/custom-assets.md new file mode 100644 index 0000000..117d12a --- /dev/null +++ b/docs/configuration/custom-assets.md @@ -0,0 +1,31 @@ +# Custom assets + +To add custom assets to your Opengist instance, you can use the `$opengist-home/custom` directory (where `$opengist-home` is the directory where Opengist stores its data). + +### Logo / Favicon + +To add a custom logo or favicon, you can add your own image file to the `$opengist-home/custom` directory, then define the relative path in the config. + +For example, if you have a logo file `logo.png` in the `$opengist-home/custom` directory, you can set the logo path in the config as follows: + +#### YAML +```yaml +custom.logo: logo.png +``` + +#### Environment variable +```sh +export OG_CUSTOM_LOGO=logo.png +``` + +Same as the favicon: + +#### YAML +```yaml +custom.favicon: favicon.png +``` + +#### Environment variable +```sh +export OG_CUSTOM_FAVICON=favicon.png +``` \ No newline at end of file diff --git a/docs/configuration/custom-links.md b/docs/configuration/custom-links.md new file mode 100644 index 0000000..e6c2b25 --- /dev/null +++ b/docs/configuration/custom-links.md @@ -0,0 +1,38 @@ +# Custom links + +On the footer of your Opengist instance, you can add links to custom static templates or any other website you want to link to. +This can be useful for legal information, privacy policy, or any other information you want to provide to your users. + +To add one or more links, you can add your own file to the `$opengist-home/custom` directory or set a URL, then define the relative path and its name in the config. + +For example, if you have a legal information file `legal.html` in the `$opengist-home/custom` directory, and also wish to add a link to a Gitea instance, you can set the link in the config as follows: + +#### YAML +```yaml +custom.static-links: + - name: Legal notices + path: legal.html + - name: Gitea + path: https://gitea.com +``` + +#### Environment variable +```sh +OG_CUSTOM_STATIC_LINK_0_NAME="Legal Notices" \ +OG_CUSTOM_STATIC_LINK_0_PATH=legal.html \ +OG_CUSTOM_STATIC_LINK_1_NAME=Gitea \ +OG_CUSTOM_STATIC_LINK_1_PATH=https://gitea.com \ +./opengist +``` + +## Templating custom HTML pages + +In the start and end of the custom HTML files, you can use the syntax to include the header and footer of the Opengist instance: + +```html +{{ template "header" . }} + + + +{{ template "footer" . }} +``` \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go index b49d5a4..eef0367 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -63,8 +63,14 @@ type config struct { OIDCSecret string `yaml:"oidc.secret" env:"OG_OIDC_SECRET"` OIDCDiscoveryUrl string `yaml:"oidc.discovery-url" env:"OG_OIDC_DISCOVERY_URL"` - CustomLogo string `yaml:"custom.logo" env:"OG_CUSTOM_LOGO"` - CustomFavicon string `yaml:"custom.favicon" env:"OG_CUSTOM_FAVICON"` + CustomLogo string `yaml:"custom.logo" env:"OG_CUSTOM_LOGO"` + CustomFavicon string `yaml:"custom.favicon" env:"OG_CUSTOM_FAVICON"` + StaticLinks []StaticLink `yaml:"custom.static-links" env:"OG_CUSTOM_STATIC_LINK"` +} + +type StaticLink struct { + Name string `yaml:"name" env:"OG_CUSTOM_STATIC_LINK_#_NAME"` + Path string `yaml:"path" env:"OG_CUSTOM_STATIC_LINK_#_PATH"` } func configWithDefaults() (*config, error) { @@ -129,7 +135,6 @@ func InitConfig(configPath string, out io.Writer) error { if err = os.Setenv("OG_OPENGIST_HOME_INTERNAL", GetHomeDir()); err != nil { return err } - return nil } @@ -246,22 +251,63 @@ func loadConfigFromEnv(c *config, out io.Writer) error { } envValue := os.Getenv(strings.ToUpper(tag)) - if envValue == "" { + if envValue == "" && v.Field(i).Kind() != reflect.Slice { continue } switch v.Field(i).Kind() { case reflect.String: v.Field(i).SetString(envValue) + envVars = append(envVars, tag) case reflect.Bool: boolVal, err := strconv.ParseBool(envValue) if err != nil { return err } v.Field(i).SetBool(boolVal) + envVars = append(envVars, tag) + case reflect.Slice: + if v.Type().Field(i).Type.Elem().Kind() == reflect.Struct { + prefix := strings.ToUpper(tag) + "_" + var sliceValue reflect.Value + elemType := v.Type().Field(i).Type.Elem() + + for index := 0; ; index++ { + allFieldsPresent := true + elemValue := reflect.New(elemType).Elem() + + for j := 0; j < elemValue.NumField() && allFieldsPresent; j++ { + elemField := elemValue.Type().Field(j) + envName := fmt.Sprintf("%s%d_%s", prefix, index, strings.ToUpper(elemField.Name)) + envValue, present := os.LookupEnv(envName) + + if !present { + allFieldsPresent = false + break + } + + envVars = append(envVars, envName) + elemValue.Field(j).SetString(envValue) + } + + if !allFieldsPresent { + break + } + + if sliceValue.Kind() != reflect.Slice { + sliceValue = reflect.MakeSlice(v.Type().Field(i).Type, 0, index+1) + } + sliceValue = reflect.Append(sliceValue, elemValue) + } + + if sliceValue.IsValid() { + v.Field(i).Set(sliceValue) + } + } + default: + return fmt.Errorf("unsupported type: %s", v.Field(i).Kind()) } - envVars = append(envVars, tag) } if len(envVars) > 0 { diff --git a/internal/web/server.go b/internal/web/server.go index 4b5c05f..c24f1d9 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/thomiceli/opengist/internal/index" "github.com/thomiceli/opengist/internal/utils" + "github.com/thomiceli/opengist/templates" htmlpkg "html" "html/template" "io" @@ -30,7 +31,6 @@ import ( "github.com/thomiceli/opengist/internal/git" "github.com/thomiceli/opengist/internal/i18n" "github.com/thomiceli/opengist/public" - "github.com/thomiceli/opengist/templates" "golang.org/x/text/language" ) @@ -138,6 +138,10 @@ var ( }, "addMetadataToSearchQuery": addMetadataToSearchQuery, "indexEnabled": index.Enabled, + "isUrl": func(s string) bool { + _, err := url.ParseRequestURI(s) + return err == nil + }, } ) @@ -186,9 +190,22 @@ func NewServer(isDev bool) *Server { e.Use(middleware.Recover()) e.Use(middleware.Secure()) - e.Renderer = &Template{ - templates: template.Must(template.New("t").Funcs(fm).ParseFS(templates.Files, "*/*.html")), + t := template.Must(template.New("t").Funcs(fm).ParseFS(templates.Files, "*/*.html")) + customPattern := filepath.Join(config.GetHomeDir(), "custom", "*.html") + matches, err := filepath.Glob(customPattern) + if err != nil { + log.Fatal().Err(err).Msg("Failed to check for custom templates") } + if len(matches) > 0 { + t, err = t.ParseGlob(customPattern) + if err != nil { + log.Fatal().Err(err).Msg("Failed to parse custom templates") + } + } + e.Renderer = &Template{ + templates: t, + } + e.HTTPErrorHandler = func(er error, ctx echo.Context) { if err, ok := er.(*echo.HTTPError); ok { if err.Code >= 500 { @@ -211,14 +228,6 @@ func NewServer(isDev bool) *Server { if !dev { parseManifestEntries() } - customFs := os.DirFS(filepath.Join(config.GetHomeDir(), "custom")) - e.GET("/assets/*", func(c echo.Context) error { - if _, err := public.Files.Open(path.Join("assets", c.Param("*"))); !dev && err == nil { - return echo.WrapHandler(http.FileServer(http.FS(public.Files)))(c) - } - - return echo.WrapHandler(http.StripPrefix("/assets/", http.FileServer(http.FS(customFs))))(c) - }) // Web based routes g1 := e.Group("") @@ -309,6 +318,23 @@ func NewServer(isDev bool) *Server { } } + customFs := os.DirFS(filepath.Join(config.GetHomeDir(), "custom")) + e.GET("/assets/*", func(ctx echo.Context) error { + if _, err := public.Files.Open(path.Join("assets", ctx.Param("*"))); !dev && err == nil { + return echo.WrapHandler(http.FileServer(http.FS(public.Files)))(ctx) + } + + // if the custom file is an .html template, render it + if strings.HasSuffix(ctx.Param("*"), ".html") { + if err := html(ctx, ctx.Param("*")); err != nil { + return notFound("Page not found") + } + return nil + } + + return echo.WrapHandler(http.StripPrefix("/assets/", http.FileServer(http.FS(customFs))))(ctx) + }) + // Git HTTP routes if config.C.HttpGit { e.Any("/:user/:gistname/*", gitHttp, gistSoftInit) diff --git a/templates/base/base_footer.html b/templates/base/base_footer.html index 99089cd..7746da9 100644 --- a/templates/base/base_footer.html +++ b/templates/base/base_footer.html @@ -5,7 +5,7 @@ {{ define "footer" }}
-

+

{{ .locale.Tr "footer.powered-by" "Opengist" }} @@ -28,6 +28,13 @@

+ {{ if ne (len .c.StaticLinks) 0 }} +
+ {{ range $index, $value := .c.StaticLinks }} + ⋅ {{ .Name }} + {{ end }} +
+ {{ end }}