From 12e7de352fe955041550e48dce63c32edc9983f8 Mon Sep 17 00:00:00 2001 From: Nicolas Carlier Date: Mon, 19 Feb 2024 08:38:19 +0100 Subject: [PATCH] feat: UI config auto generation - refactoring of the main runtime using CMD pattern - generarte UI config at runtime - lint code --- Dockerfile | 3 + Makefile | 4 +- cmd/all/index.go | 10 ++ cmd/flags.go | 32 ++++++ cmd/helper.go | 15 +++ cmd/init-config/cmd.go | 39 +++++++ cmd/registry.go | 12 +++ cmd/serve/cmd.go | 35 +++++++ cmd/serve/server.go | 117 +++++++++++++++++++++ cmd/types.go | 8 ++ cmd/version/cmd.go | 37 +++++++ main.go | 145 ++++----------------------- pkg/api/image-proxy.go | 1 + pkg/api/index.go | 6 ++ pkg/config/config.go | 14 +-- pkg/config/defaults.go | 37 ------- pkg/config/defaults.toml | 4 +- pkg/config/types.go | 1 + pkg/config/ui.go | 28 ++++++ pkg/config/ui.js | 18 ++++ pkg/config/write.go | 31 ++++++ pkg/html/text.go | 2 +- pkg/model/article.go | 8 +- pkg/sanitizer/test/sanitizer_test.go | 8 +- pkg/scraper/external.go | 2 +- pkg/scraper/internal.go | 4 +- pkg/service/users.go | 2 +- pkg/template/fast/fasttemplate.go | 2 +- pkg/version/version.go | 4 - ui/README.md | 2 +- ui/public/config.js | 9 +- ui/src/@types/readflow.d.ts | 3 +- ui/src/config.ts | 2 +- 33 files changed, 442 insertions(+), 203 deletions(-) create mode 100644 cmd/all/index.go create mode 100644 cmd/flags.go create mode 100644 cmd/helper.go create mode 100644 cmd/init-config/cmd.go create mode 100644 cmd/registry.go create mode 100644 cmd/serve/cmd.go create mode 100644 cmd/serve/server.go create mode 100644 cmd/types.go create mode 100644 cmd/version/cmd.go delete mode 100644 pkg/config/defaults.go create mode 100644 pkg/config/ui.go create mode 100644 pkg/config/ui.js create mode 100644 pkg/config/write.go diff --git a/Dockerfile b/Dockerfile index f97e0a07f..5e213c05f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -67,3 +67,6 @@ EXPOSE 8080 9090 # Define entrypoint ENTRYPOINT [ "readflow" ] + +# Define command +CMD [ "serve" ] diff --git a/Makefile b/Makefile index 76275ec3b..2f8bce470 100644 --- a/Makefile +++ b/Makefile @@ -159,8 +159,8 @@ distribution: ## Start development server (aka: a test database instance) dev-server: - docker-compose -f docker-compose.dev.yml down - docker-compose -f docker-compose.dev.yml up + docker compose -f docker-compose.dev.yml down + docker compose -f docker-compose.dev.yml up .PHONY: dev-server ## Deploy containers to Docker host diff --git a/cmd/all/index.go b/cmd/all/index.go new file mode 100644 index 000000000..9bbf836a5 --- /dev/null +++ b/cmd/all/index.go @@ -0,0 +1,10 @@ +package all + +import ( + // activate init-config command + _ "github.com/ncarlier/readflow/cmd/init-config" + // activate serve command + _ "github.com/ncarlier/readflow/cmd/serve" + // activate version command + _ "github.com/ncarlier/readflow/cmd/version" +) diff --git a/cmd/flags.go b/cmd/flags.go new file mode 100644 index 000000000..8855b7f5d --- /dev/null +++ b/cmd/flags.go @@ -0,0 +1,32 @@ +package cmd + +import ( + "flag" + "fmt" + "os" +) + +var ( + // ConfigFlag is the flag used to load the config file + ConfigFlag string +) + +func init() { + defaultValue := "" + if value, ok := os.LookupEnv("READFLOW_CONFIG"); ok { + defaultValue = value + } + flag.StringVar(&ConfigFlag, "c", defaultValue, "Configuration file to load [ENV: READFLOW_CONFIG]") + flag.Usage = func() { + out := flag.CommandLine.Output() + fmt.Fprintf(out, "readflow is a news-reading (or read-it-later) solution focused on versatility and simplicity.\n") + fmt.Fprintf(out, "\nUsage:\n readflow [flags] [command]\n") + fmt.Fprintf(out, "\nAvailable Commands:\n") + for _, c := range Commands { + c.Usage() + } + fmt.Fprintf(out, "\nFlags:\n") + flag.PrintDefaults() + fmt.Fprintf(out, "\nUse \"reaflow [command] --help\" for more information about a command.\n\n") + } +} diff --git a/cmd/helper.go b/cmd/helper.go new file mode 100644 index 000000000..b91033593 --- /dev/null +++ b/cmd/helper.go @@ -0,0 +1,15 @@ +package cmd + +import "strings" + +// GetFirstCommand restun first command of argument list +func GetFirstCommand(args []string) (name string, index int) { + for idx, arg := range args { + if strings.HasPrefix(arg, "-") { + // ignore flags + continue + } + return arg, idx + } + return "", -1 +} diff --git a/cmd/init-config/cmd.go b/cmd/init-config/cmd.go new file mode 100644 index 000000000..082b30b35 --- /dev/null +++ b/cmd/init-config/cmd.go @@ -0,0 +1,39 @@ +package initconfig + +import ( + "flag" + "fmt" + + "github.com/ncarlier/readflow/cmd" + "github.com/ncarlier/readflow/pkg/config" +) + +const cmdName = "init-config" + +type InitConfigCmd struct { + filename string + flagSet *flag.FlagSet +} + +func (c *InitConfigCmd) Exec(args []string, conf *config.Config) error { + if err := c.flagSet.Parse(args); err != nil { + return err + } + return conf.WriteDefaultConfigFile(c.filename) +} + +func (c *InitConfigCmd) Usage() { + fmt.Fprintf(c.flagSet.Output(), " %s\tInit configuration file\n", cmdName) +} + +func newInitConfigCmd() cmd.Cmd { + c := &InitConfigCmd{ + flagSet: flag.NewFlagSet(cmdName, flag.ExitOnError), + } + c.flagSet.StringVar(&c.filename, "f", "config.toml", "Configuration file to create") + return c +} + +func init() { + cmd.Add(cmdName, newInitConfigCmd) +} diff --git a/cmd/registry.go b/cmd/registry.go new file mode 100644 index 000000000..376b2c961 --- /dev/null +++ b/cmd/registry.go @@ -0,0 +1,12 @@ +package cmd + +// Creator function for an output +type Creator func() Cmd + +// Commands registry +var Commands = map[string]Cmd{} + +// Add output to the registry +func Add(name string, creator Creator) { + Commands[name] = creator() +} diff --git a/cmd/serve/cmd.go b/cmd/serve/cmd.go new file mode 100644 index 000000000..2b51cf621 --- /dev/null +++ b/cmd/serve/cmd.go @@ -0,0 +1,35 @@ +package serve + +import ( + "flag" + "fmt" + + "github.com/ncarlier/readflow/cmd" + "github.com/ncarlier/readflow/pkg/config" +) + +const cmdName = "serve" + +type ServeCmd struct { + flagSet *flag.FlagSet +} + +func (c *ServeCmd) Exec(args []string, conf *config.Config) error { + // no args + return startServer(conf) +} + +func (c *ServeCmd) Usage() { + fmt.Fprintf(c.flagSet.Output(), " %s\t\tStart readflow server\n", cmdName) +} + +func newServeCmd() cmd.Cmd { + c := &ServeCmd{ + flagSet: flag.NewFlagSet(cmdName, flag.ExitOnError), + } + return c +} + +func init() { + cmd.Add("serve", newServeCmd) +} diff --git a/cmd/serve/server.go b/cmd/serve/server.go new file mode 100644 index 000000000..9e63ea0f9 --- /dev/null +++ b/cmd/serve/server.go @@ -0,0 +1,117 @@ +package serve + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + "time" + + "github.com/ncarlier/readflow/pkg/api" + "github.com/ncarlier/readflow/pkg/cache" + "github.com/ncarlier/readflow/pkg/config" + "github.com/ncarlier/readflow/pkg/db" + "github.com/ncarlier/readflow/pkg/exporter" + "github.com/ncarlier/readflow/pkg/exporter/pdf" + "github.com/ncarlier/readflow/pkg/metric" + "github.com/ncarlier/readflow/pkg/server" + "github.com/ncarlier/readflow/pkg/service" + "github.com/rs/zerolog/log" +) + +func startServer(conf *config.Config) error { + log.Debug().Msg("starting readflow...") + + // configure the DB + database, err := db.NewDB(conf.Database.URI) + if err != nil { + return fmt.Errorf("unable to configure the database: %w", err) + } + + // configure download cache + downloadCache, err := cache.NewDefault("readflow-downloads") + if err != nil { + return fmt.Errorf("unable to configure the downloader cache storage: %w", err) + } + + // configure the service registry + err = service.Configure(*conf, database, downloadCache) + if err != nil { + database.Close() + return fmt.Errorf("unable to configure the service registry: %w", err) + } + + // register external exporters... + if conf.PDF.ServiceProvider != "" { + log.Info().Str("provider", conf.PDF.ServiceProvider).Msg("using PDF generator service") + exporter.Register("pdf", pdf.NewPDFExporter(conf.PDF.ServiceProvider)) + } + + // create HTTP server + httpServer := server.NewHTTPServer(conf) + + // create and start metrics server + metricsServer := server.NewMetricsServer(conf) + if metricsServer != nil { + metric.StartCollectors(database) + go metricsServer.ListenAndServe() + } + + // create and start SMTP server + smtpServer := server.NewSMTPHTTPServer(conf) + if smtpServer != nil { + go smtpServer.ListenAndServe() + } + + done := make(chan bool) + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt, syscall.SIGTERM) + + go func() { + <-quit + log.Debug().Msg("shutting down readflow...") + api.Shutdown() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := httpServer.Shutdown(ctx); err != nil { + log.Fatal().Err(err).Msg("unable to gracefully shutdown the HTTP server") + } + if smtpServer != nil { + if err := smtpServer.Shutdown(ctx); err != nil { + log.Fatal().Err(err).Msg("unable to gracefully shutdown the SMTP server") + } + } + if metricsServer != nil { + metric.StopCollectors() + if err := metricsServer.Shutdown(ctx); err != nil { + log.Fatal().Err(err).Msg("unable to gracefully shutdown the metrics server") + } + } + + service.Shutdown() + + if err := downloadCache.Close(); err != nil { + log.Error().Err(err).Msg("unable to gracefully shutdown the cache storage") + } + + if err := database.Close(); err != nil { + log.Fatal().Err(err).Msg("could not gracefully shutdown database connection") + } + + close(done) + }() + + // set API health check as started + api.Start() + + // start HTTP server + httpServer.ListenAndServe() + + <-done + log.Debug().Msg("readflow stopped") + + return nil +} diff --git a/cmd/types.go b/cmd/types.go new file mode 100644 index 000000000..91f841c2a --- /dev/null +++ b/cmd/types.go @@ -0,0 +1,8 @@ +package cmd + +import "github.com/ncarlier/readflow/pkg/config" + +type Cmd interface { + Exec(args []string, conf *config.Config) error + Usage() +} diff --git a/cmd/version/cmd.go b/cmd/version/cmd.go new file mode 100644 index 000000000..c0db612d4 --- /dev/null +++ b/cmd/version/cmd.go @@ -0,0 +1,37 @@ +package initconfig + +import ( + "flag" + "fmt" + + "github.com/ncarlier/readflow/cmd" + "github.com/ncarlier/readflow/pkg/config" + "github.com/ncarlier/readflow/pkg/version" +) + +const cmdName = "version" + +type VersionCmd struct { + flagSet *flag.FlagSet +} + +func (c *VersionCmd) Exec(args []string, conf *config.Config) error { + // no args + version.Print() + return nil +} + +func (c *VersionCmd) Usage() { + fmt.Fprintf(c.flagSet.Output(), " %s\tDisplay version\n", cmdName) +} + +func newVersionCmd() cmd.Cmd { + c := &VersionCmd{ + flagSet: flag.NewFlagSet(cmdName, flag.ExitOnError), + } + return c +} + +func init() { + cmd.Add("version", newVersionCmd) +} diff --git a/main.go b/main.go index 96fd0b501..44d44de6c 100644 --- a/main.go +++ b/main.go @@ -4,59 +4,27 @@ package main //go:generate gofmt -s -w autogen/db/postgres/db_sql_migration.go import ( - "context" "flag" "fmt" "os" - "os/signal" - "syscall" - "time" - "github.com/ncarlier/readflow/pkg/api" - "github.com/ncarlier/readflow/pkg/cache" + "github.com/ncarlier/readflow/cmd" "github.com/ncarlier/readflow/pkg/config" - "github.com/ncarlier/readflow/pkg/db" - "github.com/ncarlier/readflow/pkg/exporter" - "github.com/ncarlier/readflow/pkg/exporter/pdf" "github.com/ncarlier/readflow/pkg/logger" - "github.com/ncarlier/readflow/pkg/metric" - "github.com/ncarlier/readflow/pkg/server" - "github.com/ncarlier/readflow/pkg/service" - "github.com/ncarlier/readflow/pkg/version" "github.com/rs/zerolog/log" -) -func init() { - flag.Usage = func() { - fmt.Fprintf(os.Stderr, "Usage: readflow OPTIONS\n\n") - fmt.Fprintf(os.Stderr, "Options:\n") - flag.PrintDefaults() - } -} + _ "github.com/ncarlier/readflow/cmd/all" +) func main() { // parse command line flag.Parse() - // show version if asked - if *version.ShowVersion { - version.Print() - os.Exit(0) - } - - // init configuration file - if config.InitConfigFlag != nil && *config.InitConfigFlag != "" { - if err := config.WriteConfigFile(*config.InitConfigFlag); err != nil { - log.Fatal().Err(err).Msg("unable to init configuration file") - } - os.Exit(0) - } - - // load configuration file + // load configuration conf := config.NewConfig() - if config.ConfigFileFlag != nil && *config.ConfigFileFlag != "" { - if err := conf.LoadFile(*config.ConfigFileFlag); err != nil { - log.Fatal().Err(err).Msg("unable to load configuration file") + if cmd.ConfigFlag != "" { + if err := conf.LoadFile(cmd.ConfigFlag); err != nil { + log.Fatal().Err(err).Str("filename", cmd.ConfigFlag).Msg("unable to load configuration file") } } @@ -66,95 +34,18 @@ func main() { // configure the logger logger.Configure(conf.Log.Level, conf.Log.Format, conf.Integration.Sentry.DSN) - log.Debug().Msg("starting readflow...") - - // configure the DB - database, err := db.NewDB(conf.Database.URI) - if err != nil { - log.Fatal().Err(err).Msg("unable to configure the database") - } - - // configure download cache - downloadCache, err := cache.NewDefault("readflow-downloads") - if err != nil { - log.Fatal().Err(err).Msg("unable to configure the downloader cache storage") - } - - // configure the service registry - err = service.Configure(*conf, database, downloadCache) - if err != nil { - database.Close() - log.Fatal().Err(err).Msg("unable to configure the service registry") - } - - // register external exporters... - if conf.PDF.ServiceProvider != "" { - log.Info().Str("provider", conf.PDF.ServiceProvider).Msg("using PDF generator service") - exporter.Register("pdf", pdf.NewPDFExporter(conf.PDF.ServiceProvider)) - } - - // create HTTP server - httpServer := server.NewHTTPServer(conf) + args := flag.Args() + commandLabel, idx := cmd.GetFirstCommand(args) - // create and start metrics server - metricsServer := server.NewMetricsServer(conf) - if metricsServer != nil { - metric.StartCollectors(database) - go metricsServer.ListenAndServe() - } - - // create and start SMTP server - smtpServer := server.NewSMTPHTTPServer(conf) - if smtpServer != nil { - go smtpServer.ListenAndServe() - } - - done := make(chan bool) - quit := make(chan os.Signal, 1) - signal.Notify(quit, os.Interrupt, syscall.SIGTERM) - - go func() { - <-quit - log.Debug().Msg("shutting down readflow...") - api.Shutdown() - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - if err := httpServer.Shutdown(ctx); err != nil { - log.Fatal().Err(err).Msg("unable to gracefully shutdown the HTTP server") - } - if smtpServer != nil { - if err := smtpServer.Shutdown(ctx); err != nil { - log.Fatal().Err(err).Msg("unable to gracefully shutdown the SMTP server") - } + if command, ok := cmd.Commands[commandLabel]; ok { + if err := command.Exec(args[idx+1:], conf); err != nil { + log.Fatal().Err(err).Str("command", commandLabel).Msg("error during command execution") } - if metricsServer != nil { - metric.StopCollectors() - if err := metricsServer.Shutdown(ctx); err != nil { - log.Fatal().Err(err).Msg("unable to gracefully shutdown the metrics server") - } + } else { + if commandLabel != "" { + fmt.Fprintf(os.Stderr, "undefined command: %s\n", commandLabel) } - - service.Shutdown() - - if err := downloadCache.Close(); err != nil { - log.Error().Err(err).Msg("unable to gracefully shutdown the cache storage") - } - - if err := database.Close(); err != nil { - log.Fatal().Err(err).Msg("could not gracefully shutdown database connection") - } - - close(done) - }() - - // set API health check as started - api.Start() - - // start HTTP server - httpServer.ListenAndServe() - - <-done - log.Debug().Msg("readflow stopped") + flag.Usage() + os.Exit(0) + } } diff --git a/pkg/api/image-proxy.go b/pkg/api/image-proxy.go index c03bc1e7c..1bfed0899 100644 --- a/pkg/api/image-proxy.go +++ b/pkg/api/image-proxy.go @@ -35,6 +35,7 @@ func imgProxyHandler(conf *config.Config) http.Handler { _, opts, src, err := decodeImageProxyPath(img) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) + return } logger := log.With().Str("src", src).Str("opts", opts).Logger() diff --git a/pkg/api/index.go b/pkg/api/index.go index af8de3da1..cc92e871f 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -2,6 +2,7 @@ package api import ( "net/http" + "path" "github.com/ncarlier/readflow/pkg/config" "github.com/rs/zerolog/log" @@ -11,6 +12,11 @@ import ( func index(conf *config.Config) http.Handler { if conf.UI.Directory != "" { log.Debug().Str("location", conf.UI.Directory).Msg("serving UI") + // build UI config file from env variables + configFilename := path.Join(conf.UI.Directory, "config.js") + if err := conf.WriteUIConfigFile(configFilename); err != nil { + log.Fatal().Err(err).Str("filename", configFilename).Msg("failed to create UI config file") + } return http.FileServer(http.Dir(conf.UI.Directory)) } return http.RedirectHandler("/info", http.StatusSeeOther) diff --git a/pkg/config/config.go b/pkg/config/config.go index 1b76cb034..15fee0a24 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,7 +1,6 @@ package config import ( - "flag" "io" "os" @@ -9,9 +8,6 @@ import ( "github.com/imdario/mergo" ) -// ConfigFile is the flag used to load the config file -var ConfigFileFlag *string - // NewConfig create new configuration func NewConfig() *Config { c := &Config{ @@ -30,7 +26,7 @@ func NewConfig() *Config { Hostname: "localhost", }, AuthN: AuthNConfig{ - Method: "oidc", + Method: "mock", OIDC: AuthNOIDCConfig{ Issuer: "https://accounts.readflow.app", }, @@ -92,11 +88,3 @@ func (c *Config) LoadFile(path string) error { return nil } - -func init() { - defaultValue := "" - if value, ok := os.LookupEnv("READFLOW_CONFIG"); ok { - defaultValue = value - } - ConfigFileFlag = flag.String("config", defaultValue, "Configuration file to load [ENV: READFLOW_CONFIG]") -} diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go deleted file mode 100644 index 318267bea..000000000 --- a/pkg/config/defaults.go +++ /dev/null @@ -1,37 +0,0 @@ -package config - -import ( - "embed" - "flag" - "io" - "os" -) - -// Content assets -// -//go:embed defaults.toml -var ConfigFile embed.FS - -// InitConfigFlag is the flag used to initialize the config file -var InitConfigFlag = flag.String("init-config", "", "Initialize configuration file") - -// WriteConfigFile write configuration file -func WriteConfigFile(filename string) error { - src, err := ConfigFile.Open("defaults.toml") - if err != nil { - return err - } - defer src.Close() - - dst, err := os.Create(filename) - if err != nil { - return err - } - defer dst.Close() - - _, err = io.Copy(dst, src) - if err != nil { - return err - } - return nil -} diff --git a/pkg/config/defaults.toml b/pkg/config/defaults.toml index 923aa1fee..3b5bcf1da 100644 --- a/pkg/config/defaults.toml +++ b/pkg/config/defaults.toml @@ -51,7 +51,7 @@ hostname = "${READFLOW_SMTP_HOSTNAME}" # - `basic`: Basic Authentication using htpasswd file # - `proxy`: Proxied authentication using specific header as username # - `oidc`: OpenID Connect authentification using JWT access token validation -# Default: "oidc" +# Default: "mock" method = "${READFLOW_AUTHN_METHOD}" ## Administrators usernames # Comma separated list of username @@ -79,6 +79,8 @@ directory = "${READFLOW_UI_DIRECTORY}" ## UI public URL # Default: "https://localhost:8080/ui" public_url = "${READFLOW_UI_PUBLIC_URL}" +## UI client ID (when using OpenID Connect issuer) +client_id = "${READFLOW_UI_CLIENT_ID}" [hash] ## Secret key used by hash algorythms (hex-encoded) diff --git a/pkg/config/types.go b/pkg/config/types.go index 8046032f7..bde7635b0 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -77,6 +77,7 @@ type AuthNBasicConfig struct { type UIConfig struct { Directory string `toml:"directory"` PublicURL string `toml:"public_url"` + ClientID string `toml:"client_id"` } // HashConfig for hash configuration section diff --git a/pkg/config/ui.go b/pkg/config/ui.go new file mode 100644 index 000000000..4a14a07c1 --- /dev/null +++ b/pkg/config/ui.go @@ -0,0 +1,28 @@ +package config + +import ( + "embed" + "os" + "text/template" +) + +// Content assets +// +//go:embed ui.js +var UIConfigFile embed.FS + +// WriteUIConfigFile write configuration file +func (c *Config) WriteUIConfigFile(filename string) error { + tmpl, err := template.New("ui.js").ParseFS(UIConfigFile, "ui.js") + if err != nil { + return err + } + + dst, err := os.Create(filename) + if err != nil { + return err + } + defer dst.Close() + + return tmpl.Execute(dst, c) +} diff --git a/pkg/config/ui.js b/pkg/config/ui.js new file mode 100644 index 000000000..652f88681 --- /dev/null +++ b/pkg/config/ui.js @@ -0,0 +1,18 @@ +// readflow UI runtime configuration +const __READFLOW_CONFIG__ = { + // API base URL, default if empty + // Values: URL (ex: `https://api.readflow.ap`) + // Default: '' + // Default can be overridden by setting ${REACT_APP_API_ROOT} env variable during build time + apiBaseUrl: '{{ .HTTP.PublicURL }}', + // Authorithy, default if empty + // Values: URL if using OIDC (ex: `https://accounts.readflow.app`), `none` otherwise + // Default: `none` + // Default can be overridden by setting ${REACT_APP_AUTHORITY} env variable during build time + authority: '{{ if eq .AuthN.Method "oidc" }} {{ .AuthN.OIDC.Issuer }} {{ else }} "none" {{ end }}', + // OpenID Connect client ID, default if empty + // Values: string (ex: `232148523175444487@readflow.app`) + // Default: '' + // Default can be overridden by setting ${REACT_APP_CLIENT_ID} env variable during build time + client_id: '{{ .UI.ClientID }}', +} \ No newline at end of file diff --git a/pkg/config/write.go b/pkg/config/write.go new file mode 100644 index 000000000..45b10a6af --- /dev/null +++ b/pkg/config/write.go @@ -0,0 +1,31 @@ +package config + +import ( + "embed" + "io" + "os" +) + +//go:embed defaults.toml +var configFile embed.FS + +// WriteDefaultConfigFile write default configuration file +func (c *Config) WriteDefaultConfigFile(filename string) error { + src, err := configFile.Open("defaults.toml") + if err != nil { + return err + } + defer src.Close() + + dst, err := os.Create(filename) + if err != nil { + return err + } + defer dst.Close() + + _, err = io.Copy(dst, src) + if err != nil { + return err + } + return nil +} diff --git a/pkg/html/text.go b/pkg/html/text.go index b5f54f70b..5b8baac7f 100644 --- a/pkg/html/text.go +++ b/pkg/html/text.go @@ -30,7 +30,7 @@ func HTML2Text(content string) (string, error) { continue } content := html.UnescapeString(string(tokenizer.Text())) - if len(content) > 0 { + if content != "" { text.WriteString(content) } if newLineTags.MatchString(token.Data) { diff --git a/pkg/model/article.go b/pkg/model/article.go index daeef081f..7917a0a60 100644 --- a/pkg/model/article.go +++ b/pkg/model/article.go @@ -158,8 +158,8 @@ func (b *ArticleCreateFormBuilder) Random() *ArticleCreateFormBuilder { b.form.Title = gofakeit.Sentence(3) text := gofakeit.Paragraph(2, 2, 5, ".") b.form.Text = &text - html := fmt.Sprintf("

%s

", *b.form.Text) - b.form.HTML = &html + _html := fmt.Sprintf("

%s

", *b.form.Text) + b.form.HTML = &_html image := gofakeit.ImageURL(320, 200) b.form.Image = &image url := gofakeit.URL() @@ -200,8 +200,8 @@ func (b *ArticleCreateFormBuilder) Text(text string) *ArticleCreateFormBuilder { } // HTML set article HTML -func (b *ArticleCreateFormBuilder) HTML(html string) *ArticleCreateFormBuilder { - b.form.HTML = &html +func (b *ArticleCreateFormBuilder) HTML(_html string) *ArticleCreateFormBuilder { + b.form.HTML = &_html return b } diff --git a/pkg/sanitizer/test/sanitizer_test.go b/pkg/sanitizer/test/sanitizer_test.go index 8e016a6ec..f43c2229b 100644 --- a/pkg/sanitizer/test/sanitizer_test.go +++ b/pkg/sanitizer/test/sanitizer_test.go @@ -20,19 +20,19 @@ var tests = []struct { func TestSanitizer(t *testing.T) { bl, err := sanitizer.NewBlockList("file://block-list.txt", sanitizer.DefaultBlockList) assert.Nil(t, err) - sanitizer := sanitizer.NewSanitizer(bl) + san := sanitizer.NewSanitizer(bl) for _, tt := range tests { - cleaned := sanitizer.Sanitize(tt.content) + cleaned := san.Sanitize(tt.content) assert.Equal(t, tt.expectation, cleaned) } } func TestSanitizerWithoutBlockList(t *testing.T) { - sanitizer := sanitizer.NewSanitizer(nil) + san := sanitizer.NewSanitizer(nil) for idx, tt := range tests { - cleaned := sanitizer.Sanitize(tt.content) + cleaned := san.Sanitize(tt.content) if idx == 0 || idx == 3 { assert.NotEqual(t, tt.expectation, cleaned) } else { diff --git a/pkg/scraper/external.go b/pkg/scraper/external.go index 07bd48416..a92d0c307 100644 --- a/pkg/scraper/external.go +++ b/pkg/scraper/external.go @@ -43,7 +43,7 @@ func (ws extrenalWebScraper) Scrap(ctx context.Context, rawurl string) (*WebPage } func (ws extrenalWebScraper) scrap(ctx context.Context, rawurl string) (*WebPage, error) { - req, err := http.NewRequestWithContext(ctx, "GET", ws.uri, nil) + req, err := http.NewRequestWithContext(ctx, "GET", ws.uri, http.NoBody) if err != nil { return nil, err } diff --git a/pkg/scraper/internal.go b/pkg/scraper/internal.go index fd9ec0d9e..e250e357b 100644 --- a/pkg/scraper/internal.go +++ b/pkg/scraper/internal.go @@ -110,7 +110,7 @@ func (ws internalWebScraper) Scrap(ctx context.Context, rawurl string) (*WebPage } func (ws internalWebScraper) getContentType(ctx context.Context, rawurl string) (string, error) { - req, err := http.NewRequest("HEAD", rawurl, nil) + req, err := http.NewRequest("HEAD", rawurl, http.NoBody) if err != nil { return "", err } @@ -124,7 +124,7 @@ func (ws internalWebScraper) getContentType(ctx context.Context, rawurl string) } func (ws internalWebScraper) get(ctx context.Context, rawurl string) (*http.Response, error) { - req, err := http.NewRequest("GET", rawurl, nil) + req, err := http.NewRequest("GET", rawurl, http.NoBody) if err != nil { return nil, err } diff --git a/pkg/service/users.go b/pkg/service/users.go index 6fc8c4483..f9effd518 100644 --- a/pkg/service/users.go +++ b/pkg/service/users.go @@ -93,7 +93,7 @@ func (reg *Registry) DeleteAccount(ctx context.Context) (bool, error) { if err != nil { return false, err } - if err = reg.db.DeleteUser(*user); err != nil { + if err := reg.db.DeleteUser(*user); err != nil { return false, err } reg.events.Publish(event.NewEvent(EventDeleteUser, *user)) diff --git a/pkg/template/fast/fasttemplate.go b/pkg/template/fast/fasttemplate.go index c4ae96caf..22ebf6fc1 100644 --- a/pkg/template/fast/fasttemplate.go +++ b/pkg/template/fast/fasttemplate.go @@ -68,7 +68,7 @@ func evalTemplateFilters(value string, filters []string) (string, error) { return value, err } -func evalTemplateFilter(value string, filter string) (string, error) { +func evalTemplateFilter(value, filter string) (string, error) { switch filter { case "urlquery": return url.QueryEscape(value), nil diff --git a/pkg/version/version.go b/pkg/version/version.go index 54befac67..e2ed9be57 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -1,7 +1,6 @@ package version import ( - "flag" "fmt" ) @@ -14,9 +13,6 @@ var GitCommit = "n/a" // Built is the built date var Built = "n/a" -// ShowVersion is the flag used to print version -var ShowVersion = flag.Bool("version", false, "Print version") - // Print version to stdout func Print() { fmt.Printf(`Version: %s diff --git a/ui/README.md b/ui/README.md index 92ea23f64..d0535b434 100644 --- a/ui/README.md +++ b/ui/README.md @@ -10,7 +10,7 @@ You can configure the UI building process by setting environment variables: |----------|---------|-------------| | `REACT_APP_API_ROOT` | `/` | API base URL to use by default if runtime configuration is not set. | | `REACT_APP_AUTHORITY` | `none` | Authorithy to use by default if runtime configuration is not set. OpenID Connect authority provider URL or `none` if the authentication is delegated to another system (ex: Basic Auth). | -| `REACT_APP_CLIENT_ID` | `readflow-ui` | OpenID Connect client ID. | +| `REACT_APP_CLIENT_ID` | '' | OpenID Connect client ID. | | `REACT_APP_REDIRECT_URL` | `/login` | Page to redirect unauthenticated clients to. | Example: diff --git a/ui/public/config.js b/ui/public/config.js index 6846e8b5b..f543b3e16 100644 --- a/ui/public/config.js +++ b/ui/public/config.js @@ -6,8 +6,13 @@ const __READFLOW_CONFIG__ = { // Default can be overridden by setting ${REACT_APP_API_ROOT} env variable during build time apiBaseUrl: '', // Authorithy, default if empty - // Values: URL if using OIDC (ex: `https://login.nunux.org/auth/realms/readflow`), `none` otherwise + // Values: URL if using OIDC (ex: `https://accounts.readflow.app`), `none` otherwise // Default: `none` // Default can be overridden by setting ${REACT_APP_AUTHORITY} env variable during build time - authority: '' + authority: '', + // OpenID Connect client ID, default if empty + // Values: string (ex: `232148523175444487@readflow.app`) + // Default: '' + // Default can be overridden by setting ${REACT_APP_CLIENT_ID} env variable during build time + client_id: '', } \ No newline at end of file diff --git a/ui/src/@types/readflow.d.ts b/ui/src/@types/readflow.d.ts index f884f1d4a..990c176e1 100644 --- a/ui/src/@types/readflow.d.ts +++ b/ui/src/@types/readflow.d.ts @@ -1,4 +1,5 @@ declare const __READFLOW_CONFIG__: { apiBaseUrl: string, - authority: string + authority: string, + client_id: string } diff --git a/ui/src/config.ts b/ui/src/config.ts index 452eb3a63..993c992ff 100644 --- a/ui/src/config.ts +++ b/ui/src/config.ts @@ -5,7 +5,7 @@ export const API_BASE_URL = __READFLOW_CONFIG__.apiBaseUrl || process.env.REACT_ export const AUTHORITY = __READFLOW_CONFIG__.authority || process.env.REACT_APP_AUTHORITY || 'none' // OIDC client ID -export const CLIENT_ID = process.env.REACT_APP_CLIENT_ID || '232148523175444487@readflow.app' +export const CLIENT_ID = __READFLOW_CONFIG__.client_id || process.env.REACT_APP_CLIENT_ID || '' // Unauthenticated user redirect export const REDIRECT_URL = process.env.REACT_APP_REDIRECT_URL || '/login'