diff --git a/README.md b/README.md index 41748e3..50fbb37 100644 --- a/README.md +++ b/README.md @@ -55,9 +55,11 @@ For passwordless login following routes are available: | /auth/refresh | POST | | Authorization: "Bearer refresh_token" | refresh JWTs | | /auth/logout | POST | | Authorizaiton: "Bearer refresh_token" | logout from this device | +Outgoing emails containing the login token will be printed to stdout if no valid email smtp settings are provided by environment variables (see dev.env). If _EMAIL_SMTP_HOST_ is set but the host can not be reached the application will exit immediately at start. + ### Example API -The example api follows the patterns from the [chi rest example](https://github.com/go-chi/chi/tree/master/_examples/rest). Besides /auth/_the API provides two main routes /api/_ and /admin/\*, as an example to separate application and administration context. The latter requires to be logged in as administrator by providing the respective JWT in Authorization Header. +The example api follows the patterns from the [chi rest example](https://github.com/go-chi/chi/tree/master/_examples/rest). Besides _/auth_ routes the API provides two main routes for _/api_ and _/admin_ requests, the latter requires to be logged in as administrator by providing the respective JWT in Authorization Header. Check [routes.md](routes.md) for a generated overview of the provided API routes. @@ -73,18 +75,15 @@ If you want to access the api from a client that is served from a different host #### Demo client application -For demonstration of the login and account management features this API serves a demo [Vue.js](https://vuejs.org) PWA. The client's source code can be found [here](https://github.com/dhax/go-base-vue). Build and put it into the api's _./public_ folder, or use the live development server (requires ENABLE_CORS environment variable set to true). +A deployed demo version can also be found at [https://go-base.leapcell.app/](https://go-base.leapcell.app/) -Outgoing emails containing the login token will be print to stdout if no valid email smtp settings are provided by environment variables (see table below). If _EMAIL_SMTP_HOST_ is set but the host can not be reached the application will exit immediately at start. +For demonstration of the login and account management features this API serves a demo [Vue.js](https://vuejs.org) PWA. It's source code can be found [here](https://github.com/dhax/go-base-vue). You can build and put it into the api's _./public_ folder, or use the live development server (requires ENABLE_CORS environment variable set to true). Use one of the following bootstrapped users for login: - (has access to admin panel) - -TODO: deploy somewhere else... -A deployed version can also be found on [Heroku](https://govue.herokuapp.com) - [godoc]: https://godoc.org/github.com/dhax/go-base [godoc badge]: https://godoc.org/github.com/dhax/go-base?status.svg [goreportcard]: https://goreportcard.com/report/github.com/dhax/go-base diff --git a/api/admin/api.go b/api/admin/api.go index 75b4738..14eb986 100644 --- a/api/admin/api.go +++ b/api/admin/api.go @@ -47,6 +47,7 @@ func (a *API) Router() *chi.Mux { r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello Admin")) + log(r).Debug("admin access") }) r.Mount("/accounts", a.Accounts.router()) diff --git a/api/api.go b/api/api.go index 1c47655..af2f4ba 100644 --- a/api/api.go +++ b/api/api.go @@ -78,8 +78,8 @@ func New(enableCORS bool) (*chi.Mux, error) { r.Mount("/api", appAPI.Router()) }) - r.Get("/ping", func(w http.ResponseWriter, _ *http.Request) { - w.Write([]byte("pong")) + r.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("ok")) }) r.Get("/*", SPAHandler("public")) diff --git a/auth/authorize/roles.go b/auth/authorize/roles.go index ad29b99..f10472c 100644 --- a/auth/authorize/roles.go +++ b/auth/authorize/roles.go @@ -2,6 +2,7 @@ package authorize import ( "net/http" + "slices" "github.com/go-chi/render" @@ -24,10 +25,5 @@ func RequiresRole(role string) func(next http.Handler) http.Handler { } func hasRole(role string, roles []string) bool { - for _, r := range roles { - if r == role { - return true - } - } - return false + return slices.Contains(roles, role) } diff --git a/auth/jwt/claims.go b/auth/jwt/claims.go index 6d2aa53..1dcdc86 100644 --- a/auth/jwt/claims.go +++ b/auth/jwt/claims.go @@ -20,7 +20,7 @@ type AppClaims struct { } // ParseClaims parses JWT claims into AppClaims. -func (c *AppClaims) ParseClaims(claims map[string]interface{}) error { +func (c *AppClaims) ParseClaims(claims map[string]any) error { id, ok := claims["id"] if !ok { return errors.New("could not parse claim id") @@ -40,7 +40,7 @@ func (c *AppClaims) ParseClaims(claims map[string]interface{}) error { var roles []string if rl != nil { - for _, v := range rl.([]interface{}) { + for _, v := range rl.([]any) { roles = append(roles, v.(string)) } } @@ -57,7 +57,7 @@ type RefreshClaims struct { } // ParseClaims parses the JWT claims into RefreshClaims. -func (c *RefreshClaims) ParseClaims(claims map[string]interface{}) error { +func (c *RefreshClaims) ParseClaims(claims map[string]any) error { token, ok := claims["token"] if !ok { return errors.New("could not parse claim token") diff --git a/auth/jwt/tokenauth.go b/auth/jwt/tokenauth.go index 8e096d3..f3d2d0d 100644 --- a/auth/jwt/tokenauth.go +++ b/auth/jwt/tokenauth.go @@ -65,8 +65,8 @@ func (a *TokenAuth) CreateJWT(c AppClaims) (string, error) { return tokenString, err } -func ParseStructToMap(c interface{}) (map[string]interface{}, error) { - var claims map[string]interface{} +func ParseStructToMap(c any) (map[string]any, error) { + var claims map[string]any inrec, _ := json.Marshal(c) err := json.Unmarshal(inrec, &claims) if err != nil { diff --git a/auth/pwdless/api.go b/auth/pwdless/api.go index 0c43381..49f03b7 100644 --- a/auth/pwdless/api.go +++ b/auth/pwdless/api.go @@ -33,21 +33,16 @@ type AuthStorer interface { PurgeExpiredToken() error } -// Mailer defines methods to send account emails. -type Mailer interface { - LoginToken(name, email string, c email.ContentLoginToken) error -} - // Resource implements passwordless account authentication against a database. type Resource struct { LoginAuth *LoginTokenAuth TokenAuth *jwt.TokenAuth Store AuthStorer - Mailer Mailer + Mailer email.Mailer } // NewResource returns a configured authentication resource. -func NewResource(authStore AuthStorer, mailer Mailer) (*Resource, error) { +func NewResource(authStore AuthStorer, mailer email.Mailer) (*Resource, error) { loginAuth, err := NewLoginTokenAuth() if err != nil { return nil, err @@ -126,14 +121,17 @@ func (rs *Resource) login(w http.ResponseWriter, r *http.Request) { tokenURL, _ := url.JoinPath(rs.LoginAuth.loginURL, lt.Token) go func() { - content := email.ContentLoginToken{ + content := ContentLoginToken{ Email: acc.Email, Name: acc.Name, URL: tokenURL, Token: lt.Token, Expiry: lt.Expiry, } - if err := rs.Mailer.LoginToken(acc.Name, acc.Email, content); err != nil { + + msg := LoginTokenEmail(acc.Name, acc.Email, content) + + if err := rs.Mailer.Send(msg); err != nil { log(r).WithField("module", "email").Error(err) } }() diff --git a/auth/pwdless/api_test.go b/auth/pwdless/api_test.go index dd0e430..f5a0aec 100644 --- a/auth/pwdless/api_test.go +++ b/auth/pwdless/api_test.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" "net/http/httptest" "os" @@ -25,7 +24,7 @@ import ( var ( auth *Resource authStore MockAuthStore - mailer email.MockMailer + mailer *email.MockMailer ts *httptest.Server ) @@ -36,7 +35,9 @@ func TestMain(m *testing.M) { viper.SetDefault("log_level", "error") var err error - auth, err = NewResource(&authStore, &mailer) + + mailer = email.NewMockMailer() + auth, err = NewResource(&authStore, mailer) if err != nil { fmt.Println(err) os.Exit(1) @@ -73,10 +74,6 @@ func TestAuthResource_login(t *testing.T) { return &a, err } - mailer.LoginTokenFn = func(n, e string, c email.ContentLoginToken) error { - return nil - } - tests := []struct { name string email string @@ -106,11 +103,11 @@ func TestAuthResource_login(t *testing.T) { if tc.err == ErrInvalidLogin && authStore.GetAccountByEmailInvoked { t.Error("GetByLoginToken invoked for invalid email") } - if tc.err == nil && !mailer.LoginTokenInvoked { - t.Error("emailService.LoginToken not invoked") + if tc.err == nil && !mailer.SendInvoked { + t.Error("emailService.Send not invoked") } authStore.GetAccountByEmailInvoked = false - mailer.LoginTokenInvoked = false + mailer.SendInvoked = false }) } } @@ -349,7 +346,7 @@ func testRequest(t *testing.T, ts *httptest.Server, method, path string, body io } defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { t.Fatal(err) return nil, "" @@ -372,7 +369,7 @@ func genRefreshJWT(c jwt.RefreshClaims) string { return tokenString } -func encode(v interface{}) (*bytes.Buffer, error) { +func encode(v any) (*bytes.Buffer, error) { data := new(bytes.Buffer) err := json.NewEncoder(data).Encode(v) return data, err diff --git a/auth/pwdless/emails.go b/auth/pwdless/emails.go new file mode 100644 index 0000000..91cba2a --- /dev/null +++ b/auth/pwdless/emails.go @@ -0,0 +1,28 @@ +package pwdless + +import ( + "os" + "time" + + "github.com/dhax/go-base/email" +) + +// ContentLoginToken defines content for login token email template. +type ContentLoginToken struct { + Email string + Name string + URL string + Token string + Expiry time.Time +} + +// LoginTokenEmail creates and sends a login token email with provided template content. +func LoginTokenEmail(name, address string, content ContentLoginToken) email.Message { + return email.Message{ + From: email.NewEmail(os.Getenv("EMAIL_FROM_NAME"), os.Getenv("EMAIL_FROM_ADDRESS")), + To: email.NewEmail(name, address), + Subject: "Login Token", + Template: "loginToken", + Content: content, + } +} diff --git a/auth/pwdless/logintoken.go b/auth/pwdless/logintoken.go index a2a410a..4c14a78 100644 --- a/auth/pwdless/logintoken.go +++ b/auth/pwdless/logintoken.go @@ -9,9 +9,7 @@ import ( "github.com/spf13/viper" ) -var ( - errTokenNotFound = errors.New("login token not found") -) +var errTokenNotFound = errors.New("login token not found") // LoginToken is an in-memory saved token referencing an account ID and an expiry date. type LoginToken struct { diff --git a/database/admAccountStore.go b/database/admAccountStore.go index b9af692..f86a3bd 100644 --- a/database/admAccountStore.go +++ b/database/admAccountStore.go @@ -35,12 +35,12 @@ func NewAdmAccountStore(db *bun.DB) *AdmAccountStore { type AccountFilter struct { Limit int Offset int - Filter map[string]interface{} + Filter map[string]any Order []string } // NewAccountFilter returns an AccountFilter with options parsed from request url values. -func NewAccountFilter(params interface{}) (*AccountFilter, error) { +func NewAccountFilter(params any) (*AccountFilter, error) { v, ok := params.(url.Values) if !ok { return nil, ErrBadParams @@ -48,7 +48,7 @@ func NewAccountFilter(params interface{}) (*AccountFilter, error) { f := &AccountFilter{ Limit: 10, // Default limit Offset: 0, // Default offset - Filter: make(map[string]interface{}), + Filter: make(map[string]any), Order: v["order"], } // Parse limit and offset diff --git a/docker-compose.yml b/docker-compose.yml index 4b68232..7dcdfcf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,15 +10,11 @@ services: LOG_LEVEL: debug LOG_TEXTLOGGING: "true" #PORT: 3000 - DB_ADDR: postgres:5432 - #DB_USER: postgres - #DB_PASSWORD: postgres - #DB_DATABASE: postgres + DB_DSN: postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable #AUTH_JWT_EXPIRY: 1h #AUTH_JWT_REFRESH_EXPIRY: 72h #AUTH_JWT_SECRET: my secret - #SENDGRID_API_KEY: your-sendgrid-api-key - #EMAIL_FROM_ADDRESS: go-base + #EMAIL_FROM_ADDRESS: go-base@example.com #EMAIL_FROM_NAME: Go Base #EMAIL_SMTP_HOST: #EMAIL_SMTP_PORT: 465 @@ -27,7 +23,7 @@ services: ENABLE_CORS: "true" postgres: - image: postgres:16 + image: postgres:17-alpine restart: unless-stopped ports: - 5432:5432 diff --git a/email/auth.go b/email/auth.go deleted file mode 100644 index 797df1b..0000000 --- a/email/auth.go +++ /dev/null @@ -1,29 +0,0 @@ -package email - -import "time" - -// ContentLoginToken defines content for login token email template. -type ContentLoginToken struct { - Email string - Name string - URL string - Token string - Expiry time.Time -} - -// LoginToken creates and sends a login token email with provided template content. -func (m *Mailer) LoginToken(name, address string, content ContentLoginToken) error { - msg := &message{ - from: m.from, - to: NewEmail(name, address), - subject: "Login Token", - template: "loginToken", - content: content, - } - - if err := msg.parse(); err != nil { - return err - } - - return m.Send(msg) -} diff --git a/email/email.go b/email/email.go deleted file mode 100644 index 8e91636..0000000 --- a/email/email.go +++ /dev/null @@ -1,170 +0,0 @@ -// Package email provides email sending functionality. -package email - -import ( - "bytes" - "fmt" - "html/template" - "log" - "os" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/go-mail/mail" - "github.com/jaytaylor/html2text" - "github.com/spf13/viper" - "github.com/vanng822/go-premailer/premailer" -) - -var ( - debug bool - templates *template.Template -) - -// Mailer is a SMTP mailer. -type Mailer struct { - client *mail.Dialer - from Email -} - -// NewMailer returns a configured SMTP Mailer. -func NewMailer() (*Mailer, error) { - if err := parseTemplates(); err != nil { - return nil, err - } - - smtp := struct { - Host string - Port int - User string - Password string - }{ - viper.GetString("email_smtp_host"), - viper.GetInt("email_smtp_port"), - viper.GetString("email_smtp_user"), - viper.GetString("email_smtp_password"), - } - - s := &Mailer{ - client: mail.NewDialer(smtp.Host, smtp.Port, smtp.User, smtp.Password), - from: NewEmail(viper.GetString("email_from_name"), viper.GetString("email_from_address")), - } - - if smtp.Host == "" { - log.Println("SMTP host not set => printing emails to stdout") - debug = true - return s, nil - } - - d, err := s.client.Dial() - if err == nil { - d.Close() - return s, nil - } - return nil, err -} - -// Send sends the mail via smtp. -func (m *Mailer) Send(email *message) error { - if debug { - log.Println("To:", email.to.Address) - log.Println("Subject:", email.subject) - log.Println(email.text) - return nil - } - - msg := mail.NewMessage() - msg.SetAddressHeader("From", email.from.Address, email.from.Name) - msg.SetAddressHeader("To", email.to.Address, email.to.Name) - msg.SetHeader("Subject", email.subject) - msg.SetBody("text/plain", email.text) - msg.AddAlternative("text/html", email.html) - - return m.client.DialAndSend(msg) -} - -// message struct holds all parts of a specific email message. -type message struct { - from Email - to Email - subject string - template string - content interface{} - html string - text string -} - -// parse parses the corrsponding template and content -func (m *message) parse() error { - buf := new(bytes.Buffer) - if err := templates.ExecuteTemplate(buf, m.template, m.content); err != nil { - return err - } - prem, err := premailer.NewPremailerFromString(buf.String(), premailer.NewOptions()) - if err != nil { - return err - } - - html, err := prem.Transform() - if err != nil { - return err - } - m.html = html - - text, err := html2text.FromString(html, html2text.Options{PrettyTables: true}) - if err != nil { - return err - } - m.text = text - return nil -} - -// Email struct holds email address and recipient name. -type Email struct { - Name string - Address string -} - -// NewEmail returns an email address. -func NewEmail(name string, address string) Email { - return Email{ - Name: name, - Address: address, - } -} - -func parseTemplates() error { - templates = template.New("").Funcs(fMap) - return filepath.Walk("./templates", func(path string, info os.FileInfo, err error) error { - if strings.Contains(path, ".html") { - _, err = templates.ParseFiles(path) - return err - } - return err - }) -} - -var fMap = template.FuncMap{ - "formatAsDate": formatAsDate, - "formatAsDuration": formatAsDuration, -} - -func formatAsDate(t time.Time) string { - year, month, day := t.Date() - return fmt.Sprintf("%d.%d.%d", day, month, year) -} - -func formatAsDuration(t time.Time) string { - dur := time.Until(t) - hours := int(dur.Hours()) - mins := int(dur.Minutes()) - - v := "" - if hours != 0 { - v += strconv.Itoa(hours) + " hours and " - } - v += strconv.Itoa(mins) + " minutes" - return v -} diff --git a/email/mailer.go b/email/mailer.go new file mode 100644 index 0000000..3607473 --- /dev/null +++ b/email/mailer.go @@ -0,0 +1,106 @@ +// Package email provides email sending functionality. +package email + +import ( + "bytes" + "fmt" + "html/template" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/jaytaylor/html2text" + "github.com/vanng822/go-premailer/premailer" +) + +var templates *template.Template + +type Mailer interface { + Send(Message) error +} + +// Message struct holds all parts of a specific email Message. +type Message struct { + From Email + To Email + Subject string + Template string + Content any + html string + text string +} + +// parse parses the corrsponding template and content +func (m *Message) parse() error { + buf := new(bytes.Buffer) + if err := templates.ExecuteTemplate(buf, m.Template, m.Content); err != nil { + return err + } + prem, err := premailer.NewPremailerFromString(buf.String(), premailer.NewOptions()) + if err != nil { + return err + } + + html, err := prem.Transform() + if err != nil { + return err + } + m.html = html + + text, err := html2text.FromString(html, html2text.Options{PrettyTables: true}) + if err != nil { + return err + } + m.text = text + return nil +} + +// Email struct holds email address and recipient name. +type Email struct { + Name string + Address string +} + +// NewEmail returns an email address. +func NewEmail(name string, address string) Email { + return Email{ + Name: name, + Address: address, + } +} + +func parseTemplates() error { + templates = template.New("").Funcs(fMap) + return filepath.Walk("./templates", func(path string, info os.FileInfo, err error) error { + if strings.Contains(path, ".html") { + _, err = templates.ParseFiles(path) + return err + } + return err + }) +} + +var fMap = template.FuncMap{ + "formatAsDate": formatAsDate, + "formatAsDuration": formatAsDuration, +} + +func formatAsDate(t time.Time) string { + year, month, day := t.Date() + return fmt.Sprintf("%d.%d.%d", day, month, year) +} + +func formatAsDuration(t time.Time) string { + dur := time.Until(t) + hours := int(dur.Hours()) + mins := int(dur.Minutes()) + + v := "" + if hours != 0 { + v += strconv.Itoa(hours) + " hours and " + } + v += strconv.Itoa(mins) + " minutes" + return v +} diff --git a/email/mockMailer.go b/email/mockMailer.go index f70d778..cc4d481 100644 --- a/email/mockMailer.go +++ b/email/mockMailer.go @@ -1,13 +1,28 @@ package email +import "log" + // MockMailer is a mock Mailer type MockMailer struct { - LoginTokenFn func(name, email string, c ContentLoginToken) error - LoginTokenInvoked bool + SendFn func(m Message) error + SendInvoked bool } -// LoginToken is a mock for LoginToken -func (s *MockMailer) LoginToken(n, e string, c ContentLoginToken) error { - s.LoginTokenInvoked = true - return s.LoginTokenFn(n, e, c) +func logMessage(m Message) { + log.Printf("MockMailer email sent:\nSubject: %s\nTo: %s <%s>\nContext: %#v\n", m.Subject, m.To.Name, m.To.Address, m.Content) +} + +func NewMockMailer() *MockMailer { + log.Println("ATTENTION: SMTP Mailer not configured => printing emails to stdout") + return &MockMailer{ + SendFn: func(m Message) error { + logMessage(m) + return nil + }, + } +} + +func (s *MockMailer) Send(m Message) error { + s.SendInvoked = true + return s.SendFn(m) } diff --git a/email/smtp.go b/email/smtp.go new file mode 100644 index 0000000..e0ba5f3 --- /dev/null +++ b/email/smtp.go @@ -0,0 +1,63 @@ +package email + +import ( + "github.com/spf13/viper" + "github.com/wneessen/go-mail" +) + +// SMTPMailer is a SMTP mailer. +type SMTPMailer struct { + client *mail.Client + from Email +} + +// NewMailer returns a configured SMTP Mailer. +func NewMailer() (Mailer, error) { + if err := parseTemplates(); err != nil { + return nil, err + } + + smtp := struct { + Host string + Port int + User string + Password string + }{ + viper.GetString("email_smtp_host"), + viper.GetInt("email_smtp_port"), + viper.GetString("email_smtp_user"), + viper.GetString("email_smtp_password"), + } + + if smtp.Host == "" { + return NewMockMailer(), nil + } + + client, err := mail.NewClient(smtp.Host, mail.WithPort(smtp.Port), + mail.WithSMTPAuth(mail.SMTPAuthPlain), + mail.WithUsername(smtp.User), mail.WithPassword(smtp.Password)) + if err != nil { + return nil, err + } + s := &SMTPMailer{ + client: client, + from: NewEmail(viper.GetString("email_from_name"), viper.GetString("email_from_address")), + } + return s, nil +} + +// Send sends the mail via smtp. +func (m *SMTPMailer) Send(email Message) error { + if err := email.parse(); err != nil { + return err + } + + msg := mail.NewMsg() + msg.SetAddrHeader("From", email.From.Address, email.From.Name) + msg.SetAddrHeader("To", email.To.Address, email.To.Name) + msg.Subject(email.Subject) + msg.SetBodyString(mail.TypeTextPlain, email.text) + msg.AddAlternativeString(mail.TypeTextHTML, email.html) + + return m.client.DialAndSend(msg) +} diff --git a/go.mod b/go.mod index e2ed7a3..3358a51 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,15 @@ module github.com/dhax/go-base -go 1.23 +go 1.23.0 + +toolchain go1.24.1 require ( - github.com/go-chi/chi/v5 v5.2.0 + github.com/go-chi/chi/v5 v5.2.2 github.com/go-chi/cors v1.2.1 github.com/go-chi/docgen v1.3.0 github.com/go-chi/jwtauth/v5 v5.3.2 github.com/go-chi/render v1.0.3 - github.com/go-mail/mail v2.3.1+incompatible github.com/go-ozzo/ozzo-validation v3.6.0+incompatible github.com/gofrs/uuid v4.4.0+incompatible github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 @@ -22,6 +23,7 @@ require ( github.com/uptrace/bun/driver/pgdriver v1.2.7 github.com/uptrace/bun/extra/bundebug v1.2.7 github.com/vanng822/go-premailer v1.22.0 + github.com/wneessen/go-mail v0.6.2 ) require ( @@ -65,14 +67,12 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.31.0 // indirect + golang.org/x/crypto v0.36.0 // indirect golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect - gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/mail.v2 v2.3.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect mellium.im/sasl v0.3.2 // indirect ) diff --git a/go.sum b/go.sum index 53c2e5f..268a5d2 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,8 @@ github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbS github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= -github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/docgen v1.3.0 h1:dmDJ2I+EJfCTrxfgxQDwfR/OpZLTRFKe7EKB8v7yuxI= @@ -34,8 +34,6 @@ github.com/go-chi/jwtauth/v5 v5.3.2/go.mod h1:O4QvPRuZLZghl9WvfVaON+ARfGzpD2PBX/ github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= -github.com/go-mail/mail v2.3.1+incompatible h1:UzNOn0k5lpfVtO31cK3hn6I4VEVGhe3lX8AJBAxXExM= -github.com/go-mail/mail v2.3.1+incompatible/go.mod h1:VPWjmmNyRsWXQZHVHT3g0YbIINUkSmuKOiLIDkWbL6M= github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE= github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= @@ -156,6 +154,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/wneessen/go-mail v0.6.2 h1:c6V7c8D2mz868z9WJ+8zDKtUyLfZ1++uAZmo2GRFji8= +github.com/wneessen/go-mail v0.6.2/go.mod h1:L/PYjPK3/2ZlNb2/FjEBIn9n1rUWjW+Toy531oVmeb4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= @@ -166,8 +166,10 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -186,8 +188,9 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -196,6 +199,7 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -214,8 +218,10 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -228,6 +234,7 @@ golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -237,8 +244,10 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -246,15 +255,11 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= -gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= -gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/logging/logger.go b/logging/logger.go index 23666b2..5084b3e 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -12,10 +12,8 @@ import ( "github.com/spf13/viper" ) -var ( - // Logger is a configured logrus.Logger. - Logger *logrus.Logger -) +// Logger is a configured logrus.Logger. +var Logger *logrus.Logger // StructuredLogger is a structured logrus Logger. type StructuredLogger struct { @@ -90,7 +88,7 @@ type StructuredLoggerEntry struct { Logger logrus.FieldLogger } -func (l *StructuredLoggerEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) { +func (l *StructuredLoggerEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra any) { l.Logger = l.Logger.WithFields(logrus.Fields{ "resp_status": status, "resp_bytes_length": bytes, @@ -101,7 +99,7 @@ func (l *StructuredLoggerEntry) Write(status, bytes int, header http.Header, ela } // Panic prints stack trace -func (l *StructuredLoggerEntry) Panic(v interface{}, stack []byte) { +func (l *StructuredLoggerEntry) Panic(v any, stack []byte) { l.Logger = l.Logger.WithFields(logrus.Fields{ "stack": string(stack), "panic": fmt.Sprintf("%+v", v), @@ -118,14 +116,14 @@ func GetLogEntry(r *http.Request) logrus.FieldLogger { } // LogEntrySetField adds a field to the request scoped logrus.FieldLogger. -func LogEntrySetField(r *http.Request, key string, value interface{}) { +func LogEntrySetField(r *http.Request, key string, value any) { if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*StructuredLoggerEntry); ok { entry.Logger = entry.Logger.WithField(key, value) } } // LogEntrySetFields adds multiple fields to the request scoped logrus.FieldLogger. -func LogEntrySetFields(r *http.Request, fields map[string]interface{}) { +func LogEntrySetFields(r *http.Request, fields map[string]any) { if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*StructuredLoggerEntry); ok { entry.Logger = entry.Logger.WithFields(fields) }