diff --git a/README.md b/README.md index 50fbb37..41748e3 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,9 @@ 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_ 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. +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. Check [routes.md](routes.md) for a generated overview of the provided API routes. @@ -75,15 +73,18 @@ If you want to access the api from a client that is served from a different host #### Demo client application -A deployed demo version can also be found at [https://go-base.leapcell.app/](https://go-base.leapcell.app/) +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). -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). +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. 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 14eb986..75b4738 100644 --- a/api/admin/api.go +++ b/api/admin/api.go @@ -47,7 +47,6 @@ 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 af2f4ba..1c47655 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("/healthz", func(w http.ResponseWriter, _ *http.Request) { - w.Write([]byte("ok")) + r.Get("/ping", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("pong")) }) r.Get("/*", SPAHandler("public")) diff --git a/auth/authorize/roles.go b/auth/authorize/roles.go index f10472c..ad29b99 100644 --- a/auth/authorize/roles.go +++ b/auth/authorize/roles.go @@ -2,7 +2,6 @@ package authorize import ( "net/http" - "slices" "github.com/go-chi/render" @@ -25,5 +24,10 @@ func RequiresRole(role string) func(next http.Handler) http.Handler { } func hasRole(role string, roles []string) bool { - return slices.Contains(roles, role) + for _, r := range roles { + if r == role { + return true + } + } + return false } diff --git a/auth/jwt/claims.go b/auth/jwt/claims.go index 1dcdc86..6d2aa53 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]any) error { +func (c *AppClaims) ParseClaims(claims map[string]interface{}) 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]any) error { var roles []string if rl != nil { - for _, v := range rl.([]any) { + for _, v := range rl.([]interface{}) { 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]any) error { +func (c *RefreshClaims) ParseClaims(claims map[string]interface{}) 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 f3d2d0d..8e096d3 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 any) (map[string]any, error) { - var claims map[string]any +func ParseStructToMap(c interface{}) (map[string]interface{}, error) { + var claims map[string]interface{} 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 49f03b7..0c43381 100644 --- a/auth/pwdless/api.go +++ b/auth/pwdless/api.go @@ -33,16 +33,21 @@ 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 email.Mailer + Mailer Mailer } // NewResource returns a configured authentication resource. -func NewResource(authStore AuthStorer, mailer email.Mailer) (*Resource, error) { +func NewResource(authStore AuthStorer, mailer Mailer) (*Resource, error) { loginAuth, err := NewLoginTokenAuth() if err != nil { return nil, err @@ -121,17 +126,14 @@ func (rs *Resource) login(w http.ResponseWriter, r *http.Request) { tokenURL, _ := url.JoinPath(rs.LoginAuth.loginURL, lt.Token) go func() { - content := ContentLoginToken{ + content := email.ContentLoginToken{ Email: acc.Email, Name: acc.Name, URL: tokenURL, Token: lt.Token, Expiry: lt.Expiry, } - - msg := LoginTokenEmail(acc.Name, acc.Email, content) - - if err := rs.Mailer.Send(msg); err != nil { + if err := rs.Mailer.LoginToken(acc.Name, acc.Email, content); err != nil { log(r).WithField("module", "email").Error(err) } }() diff --git a/auth/pwdless/api_test.go b/auth/pwdless/api_test.go index f5a0aec..dd0e430 100644 --- a/auth/pwdless/api_test.go +++ b/auth/pwdless/api_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "io/ioutil" "net/http" "net/http/httptest" "os" @@ -24,7 +25,7 @@ import ( var ( auth *Resource authStore MockAuthStore - mailer *email.MockMailer + mailer email.MockMailer ts *httptest.Server ) @@ -35,9 +36,7 @@ func TestMain(m *testing.M) { viper.SetDefault("log_level", "error") var err error - - mailer = email.NewMockMailer() - auth, err = NewResource(&authStore, mailer) + auth, err = NewResource(&authStore, &mailer) if err != nil { fmt.Println(err) os.Exit(1) @@ -74,6 +73,10 @@ 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 @@ -103,11 +106,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.SendInvoked { - t.Error("emailService.Send not invoked") + if tc.err == nil && !mailer.LoginTokenInvoked { + t.Error("emailService.LoginToken not invoked") } authStore.GetAccountByEmailInvoked = false - mailer.SendInvoked = false + mailer.LoginTokenInvoked = false }) } } @@ -346,7 +349,7 @@ func testRequest(t *testing.T, ts *httptest.Server, method, path string, body io } defer resp.Body.Close() - respBody, err := io.ReadAll(resp.Body) + respBody, err := ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) return nil, "" @@ -369,7 +372,7 @@ func genRefreshJWT(c jwt.RefreshClaims) string { return tokenString } -func encode(v any) (*bytes.Buffer, error) { +func encode(v interface{}) (*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 deleted file mode 100644 index 91cba2a..0000000 --- a/auth/pwdless/emails.go +++ /dev/null @@ -1,28 +0,0 @@ -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 4c14a78..a2a410a 100644 --- a/auth/pwdless/logintoken.go +++ b/auth/pwdless/logintoken.go @@ -9,7 +9,9 @@ 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 f86a3bd..b9af692 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]any + Filter map[string]interface{} Order []string } // NewAccountFilter returns an AccountFilter with options parsed from request url values. -func NewAccountFilter(params any) (*AccountFilter, error) { +func NewAccountFilter(params interface{}) (*AccountFilter, error) { v, ok := params.(url.Values) if !ok { return nil, ErrBadParams @@ -48,7 +48,7 @@ func NewAccountFilter(params any) (*AccountFilter, error) { f := &AccountFilter{ Limit: 10, // Default limit Offset: 0, // Default offset - Filter: make(map[string]any), + Filter: make(map[string]interface{}), Order: v["order"], } // Parse limit and offset diff --git a/docker-compose.yml b/docker-compose.yml index 7dcdfcf..4b68232 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,11 +10,15 @@ services: LOG_LEVEL: debug LOG_TEXTLOGGING: "true" #PORT: 3000 - DB_DSN: postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable + DB_ADDR: postgres:5432 + #DB_USER: postgres + #DB_PASSWORD: postgres + #DB_DATABASE: postgres #AUTH_JWT_EXPIRY: 1h #AUTH_JWT_REFRESH_EXPIRY: 72h #AUTH_JWT_SECRET: my secret - #EMAIL_FROM_ADDRESS: go-base@example.com + #SENDGRID_API_KEY: your-sendgrid-api-key + #EMAIL_FROM_ADDRESS: go-base #EMAIL_FROM_NAME: Go Base #EMAIL_SMTP_HOST: #EMAIL_SMTP_PORT: 465 @@ -23,7 +27,7 @@ services: ENABLE_CORS: "true" postgres: - image: postgres:17-alpine + image: postgres:16 restart: unless-stopped ports: - 5432:5432 diff --git a/email/auth.go b/email/auth.go new file mode 100644 index 0000000..797df1b --- /dev/null +++ b/email/auth.go @@ -0,0 +1,29 @@ +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 new file mode 100644 index 0000000..8e91636 --- /dev/null +++ b/email/email.go @@ -0,0 +1,170 @@ +// 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 deleted file mode 100644 index 3607473..0000000 --- a/email/mailer.go +++ /dev/null @@ -1,106 +0,0 @@ -// 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 cc4d481..f70d778 100644 --- a/email/mockMailer.go +++ b/email/mockMailer.go @@ -1,28 +1,13 @@ package email -import "log" - // MockMailer is a mock Mailer type MockMailer struct { - SendFn func(m Message) error - SendInvoked bool + LoginTokenFn func(name, email string, c ContentLoginToken) error + LoginTokenInvoked bool } -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) +// 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) } diff --git a/email/smtp.go b/email/smtp.go deleted file mode 100644 index e0ba5f3..0000000 --- a/email/smtp.go +++ /dev/null @@ -1,63 +0,0 @@ -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 3358a51..e2ed7a3 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,14 @@ module github.com/dhax/go-base -go 1.23.0 - -toolchain go1.24.1 +go 1.23 require ( - github.com/go-chi/chi/v5 v5.2.2 + github.com/go-chi/chi/v5 v5.2.0 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 @@ -23,7 +22,6 @@ 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 ( @@ -67,12 +65,14 @@ 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.36.0 // indirect + golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // 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 + 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 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 268a5d2..53c2e5f 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.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= -github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +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/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,6 +34,8 @@ 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= @@ -154,8 +156,6 @@ 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,10 +166,8 @@ 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= @@ -188,9 +186,8 @@ 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= @@ -199,7 +196,6 @@ 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= @@ -218,10 +214,8 @@ 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= @@ -234,7 +228,6 @@ 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= @@ -244,10 +237,8 @@ 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= @@ -255,11 +246,15 @@ 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 5084b3e..23666b2 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -12,8 +12,10 @@ import ( "github.com/spf13/viper" ) -// Logger is a configured logrus.Logger. -var Logger *logrus.Logger +var ( + // Logger is a configured logrus.Logger. + Logger *logrus.Logger +) // StructuredLogger is a structured logrus Logger. type StructuredLogger struct { @@ -88,7 +90,7 @@ type StructuredLoggerEntry struct { Logger logrus.FieldLogger } -func (l *StructuredLoggerEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra any) { +func (l *StructuredLoggerEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) { l.Logger = l.Logger.WithFields(logrus.Fields{ "resp_status": status, "resp_bytes_length": bytes, @@ -99,7 +101,7 @@ func (l *StructuredLoggerEntry) Write(status, bytes int, header http.Header, ela } // Panic prints stack trace -func (l *StructuredLoggerEntry) Panic(v any, stack []byte) { +func (l *StructuredLoggerEntry) Panic(v interface{}, stack []byte) { l.Logger = l.Logger.WithFields(logrus.Fields{ "stack": string(stack), "panic": fmt.Sprintf("%+v", v), @@ -116,14 +118,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 any) { +func LogEntrySetField(r *http.Request, key string, value interface{}) { 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]any) { +func LogEntrySetFields(r *http.Request, fields map[string]interface{}) { if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*StructuredLoggerEntry); ok { entry.Logger = entry.Logger.WithFields(fields) }