Compare commits

..

10 commits

Author SHA1 Message Date
dependabot[bot]
7018751d63
Bump github.com/go-chi/chi/v5 from 5.2.0 to 5.2.2 (#26)
Some checks failed
/ pre (push) Has been cancelled
/ deps (push) Has been cancelled
/ lint (push) Has been cancelled
/ test (push) Has been cancelled
/ build (push) Has been cancelled
/ deploy (push) Has been cancelled
Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.2.0 to 5.2.2.
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.2.0...v5.2.2)

---
updated-dependencies:
- dependency-name: github.com/go-chi/chi/v5
  dependency-version: 5.2.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-18 22:43:31 +02:00
dhax
b393ed9b84
Update demo deplyoment url 2025-04-25 14:29:30 +02:00
dependabot[bot]
334b1ef267
Bump golang.org/x/net from 0.33.0 to 0.38.0 (#25)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.33.0 to 0.38.0.
- [Commits](https://github.com/golang/net/compare/v0.33.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.38.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-24 21:49:04 +02:00
dhax
4a0e66a864 correctly set alterative html string 2025-03-09 19:15:22 +01:00
dhax
eadf744eb4 switch to wneessen/go-mail 2025-03-09 19:07:48 +01:00
dhax
1a4a9abbbc move /ping to /healthz 2025-03-05 21:16:59 +01:00
dhax
08c09ffee7 make Mailer an interface 2025-03-05 18:37:11 +01:00
dhax
dd2412463b fix gopls lintings 2025-03-05 18:37:11 +01:00
dhax
a2e2ba39f9 deploy demo on render.com 2025-03-05 18:37:11 +01:00
dhax
dde0d727cb use postgres:17-alpine docker image
clean up db environment keys
2025-03-05 18:37:11 +01:00
20 changed files with 288 additions and 287 deletions

View file

@ -55,9 +55,11 @@ For passwordless login following routes are available:
| /auth/refresh | POST | | Authorization: "Bearer refresh_token" | refresh JWTs | | /auth/refresh | POST | | Authorization: "Bearer refresh_token" | refresh JWTs |
| /auth/logout | POST | | Authorizaiton: "Bearer refresh_token" | logout from this device | | /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 ### 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. 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 #### 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: Use one of the following bootstrapped users for login:
- <admin@example.com> (has access to admin panel) - <admin@example.com> (has access to admin panel)
- <user@example.com> - <user@example.com>
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]: https://godoc.org/github.com/dhax/go-base
[godoc badge]: https://godoc.org/github.com/dhax/go-base?status.svg [godoc badge]: https://godoc.org/github.com/dhax/go-base?status.svg
[goreportcard]: https://goreportcard.com/report/github.com/dhax/go-base [goreportcard]: https://goreportcard.com/report/github.com/dhax/go-base

View file

@ -47,6 +47,7 @@ func (a *API) Router() *chi.Mux {
r.Get("/", func(w http.ResponseWriter, r *http.Request) { r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello Admin")) w.Write([]byte("Hello Admin"))
log(r).Debug("admin access")
}) })
r.Mount("/accounts", a.Accounts.router()) r.Mount("/accounts", a.Accounts.router())

View file

@ -78,8 +78,8 @@ func New(enableCORS bool) (*chi.Mux, error) {
r.Mount("/api", appAPI.Router()) r.Mount("/api", appAPI.Router())
}) })
r.Get("/ping", func(w http.ResponseWriter, _ *http.Request) { r.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte("pong")) w.Write([]byte("ok"))
}) })
r.Get("/*", SPAHandler("public")) r.Get("/*", SPAHandler("public"))

View file

@ -2,6 +2,7 @@ package authorize
import ( import (
"net/http" "net/http"
"slices"
"github.com/go-chi/render" "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 { func hasRole(role string, roles []string) bool {
for _, r := range roles { return slices.Contains(roles, role)
if r == role {
return true
}
}
return false
} }

View file

@ -20,7 +20,7 @@ type AppClaims struct {
} }
// ParseClaims parses JWT claims into AppClaims. // 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"] id, ok := claims["id"]
if !ok { if !ok {
return errors.New("could not parse claim id") return errors.New("could not parse claim id")
@ -40,7 +40,7 @@ func (c *AppClaims) ParseClaims(claims map[string]interface{}) error {
var roles []string var roles []string
if rl != nil { if rl != nil {
for _, v := range rl.([]interface{}) { for _, v := range rl.([]any) {
roles = append(roles, v.(string)) roles = append(roles, v.(string))
} }
} }
@ -57,7 +57,7 @@ type RefreshClaims struct {
} }
// ParseClaims parses the JWT claims into RefreshClaims. // 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"] token, ok := claims["token"]
if !ok { if !ok {
return errors.New("could not parse claim token") return errors.New("could not parse claim token")

View file

@ -65,8 +65,8 @@ func (a *TokenAuth) CreateJWT(c AppClaims) (string, error) {
return tokenString, err return tokenString, err
} }
func ParseStructToMap(c interface{}) (map[string]interface{}, error) { func ParseStructToMap(c any) (map[string]any, error) {
var claims map[string]interface{} var claims map[string]any
inrec, _ := json.Marshal(c) inrec, _ := json.Marshal(c)
err := json.Unmarshal(inrec, &claims) err := json.Unmarshal(inrec, &claims)
if err != nil { if err != nil {

View file

@ -33,21 +33,16 @@ type AuthStorer interface {
PurgeExpiredToken() error 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. // Resource implements passwordless account authentication against a database.
type Resource struct { type Resource struct {
LoginAuth *LoginTokenAuth LoginAuth *LoginTokenAuth
TokenAuth *jwt.TokenAuth TokenAuth *jwt.TokenAuth
Store AuthStorer Store AuthStorer
Mailer Mailer Mailer email.Mailer
} }
// NewResource returns a configured authentication resource. // 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() loginAuth, err := NewLoginTokenAuth()
if err != nil { if err != nil {
return nil, err 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) tokenURL, _ := url.JoinPath(rs.LoginAuth.loginURL, lt.Token)
go func() { go func() {
content := email.ContentLoginToken{ content := ContentLoginToken{
Email: acc.Email, Email: acc.Email,
Name: acc.Name, Name: acc.Name,
URL: tokenURL, URL: tokenURL,
Token: lt.Token, Token: lt.Token,
Expiry: lt.Expiry, 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) log(r).WithField("module", "email").Error(err)
} }
}() }()

View file

@ -6,7 +6,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
@ -25,7 +24,7 @@ import (
var ( var (
auth *Resource auth *Resource
authStore MockAuthStore authStore MockAuthStore
mailer email.MockMailer mailer *email.MockMailer
ts *httptest.Server ts *httptest.Server
) )
@ -36,7 +35,9 @@ func TestMain(m *testing.M) {
viper.SetDefault("log_level", "error") viper.SetDefault("log_level", "error")
var err error var err error
auth, err = NewResource(&authStore, &mailer)
mailer = email.NewMockMailer()
auth, err = NewResource(&authStore, mailer)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
@ -73,10 +74,6 @@ func TestAuthResource_login(t *testing.T) {
return &a, err return &a, err
} }
mailer.LoginTokenFn = func(n, e string, c email.ContentLoginToken) error {
return nil
}
tests := []struct { tests := []struct {
name string name string
email string email string
@ -106,11 +103,11 @@ func TestAuthResource_login(t *testing.T) {
if tc.err == ErrInvalidLogin && authStore.GetAccountByEmailInvoked { if tc.err == ErrInvalidLogin && authStore.GetAccountByEmailInvoked {
t.Error("GetByLoginToken invoked for invalid email") t.Error("GetByLoginToken invoked for invalid email")
} }
if tc.err == nil && !mailer.LoginTokenInvoked { if tc.err == nil && !mailer.SendInvoked {
t.Error("emailService.LoginToken not invoked") t.Error("emailService.Send not invoked")
} }
authStore.GetAccountByEmailInvoked = false 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() defer resp.Body.Close()
respBody, err := ioutil.ReadAll(resp.Body) respBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
return nil, "" return nil, ""
@ -372,7 +369,7 @@ func genRefreshJWT(c jwt.RefreshClaims) string {
return tokenString return tokenString
} }
func encode(v interface{}) (*bytes.Buffer, error) { func encode(v any) (*bytes.Buffer, error) {
data := new(bytes.Buffer) data := new(bytes.Buffer)
err := json.NewEncoder(data).Encode(v) err := json.NewEncoder(data).Encode(v)
return data, err return data, err

28
auth/pwdless/emails.go Normal file
View file

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

View file

@ -9,9 +9,7 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
var ( var errTokenNotFound = errors.New("login token not found")
errTokenNotFound = errors.New("login token not found")
)
// LoginToken is an in-memory saved token referencing an account ID and an expiry date. // LoginToken is an in-memory saved token referencing an account ID and an expiry date.
type LoginToken struct { type LoginToken struct {

View file

@ -35,12 +35,12 @@ func NewAdmAccountStore(db *bun.DB) *AdmAccountStore {
type AccountFilter struct { type AccountFilter struct {
Limit int Limit int
Offset int Offset int
Filter map[string]interface{} Filter map[string]any
Order []string Order []string
} }
// NewAccountFilter returns an AccountFilter with options parsed from request url values. // 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) v, ok := params.(url.Values)
if !ok { if !ok {
return nil, ErrBadParams return nil, ErrBadParams
@ -48,7 +48,7 @@ func NewAccountFilter(params interface{}) (*AccountFilter, error) {
f := &AccountFilter{ f := &AccountFilter{
Limit: 10, // Default limit Limit: 10, // Default limit
Offset: 0, // Default offset Offset: 0, // Default offset
Filter: make(map[string]interface{}), Filter: make(map[string]any),
Order: v["order"], Order: v["order"],
} }
// Parse limit and offset // Parse limit and offset

View file

@ -10,15 +10,11 @@ services:
LOG_LEVEL: debug LOG_LEVEL: debug
LOG_TEXTLOGGING: "true" LOG_TEXTLOGGING: "true"
#PORT: 3000 #PORT: 3000
DB_ADDR: postgres:5432 DB_DSN: postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
#DB_USER: postgres
#DB_PASSWORD: postgres
#DB_DATABASE: postgres
#AUTH_JWT_EXPIRY: 1h #AUTH_JWT_EXPIRY: 1h
#AUTH_JWT_REFRESH_EXPIRY: 72h #AUTH_JWT_REFRESH_EXPIRY: 72h
#AUTH_JWT_SECRET: my secret #AUTH_JWT_SECRET: my secret
#SENDGRID_API_KEY: your-sendgrid-api-key #EMAIL_FROM_ADDRESS: go-base@example.com
#EMAIL_FROM_ADDRESS: go-base
#EMAIL_FROM_NAME: Go Base #EMAIL_FROM_NAME: Go Base
#EMAIL_SMTP_HOST: #EMAIL_SMTP_HOST:
#EMAIL_SMTP_PORT: 465 #EMAIL_SMTP_PORT: 465
@ -27,7 +23,7 @@ services:
ENABLE_CORS: "true" ENABLE_CORS: "true"
postgres: postgres:
image: postgres:16 image: postgres:17-alpine
restart: unless-stopped restart: unless-stopped
ports: ports:
- 5432:5432 - 5432:5432

View file

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

View file

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

106
email/mailer.go Normal file
View file

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

View file

@ -1,13 +1,28 @@
package email package email
import "log"
// MockMailer is a mock Mailer // MockMailer is a mock Mailer
type MockMailer struct { type MockMailer struct {
LoginTokenFn func(name, email string, c ContentLoginToken) error SendFn func(m Message) error
LoginTokenInvoked bool SendInvoked bool
} }
// LoginToken is a mock for LoginToken func logMessage(m Message) {
func (s *MockMailer) LoginToken(n, e string, c ContentLoginToken) error { log.Printf("MockMailer email sent:\nSubject: %s\nTo: %s <%s>\nContext: %#v\n", m.Subject, m.To.Name, m.To.Address, m.Content)
s.LoginTokenInvoked = true }
return s.LoginTokenFn(n, e, c)
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)
} }

63
email/smtp.go Normal file
View file

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

18
go.mod
View file

@ -1,14 +1,15 @@
module github.com/dhax/go-base module github.com/dhax/go-base
go 1.23 go 1.23.0
toolchain go1.24.1
require ( 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/cors v1.2.1
github.com/go-chi/docgen v1.3.0 github.com/go-chi/docgen v1.3.0
github.com/go-chi/jwtauth/v5 v5.3.2 github.com/go-chi/jwtauth/v5 v5.3.2
github.com/go-chi/render v1.0.3 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/go-ozzo/ozzo-validation v3.6.0+incompatible
github.com/gofrs/uuid v4.4.0+incompatible github.com/gofrs/uuid v4.4.0+incompatible
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 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/driver/pgdriver v1.2.7
github.com/uptrace/bun/extra/bundebug v1.2.7 github.com/uptrace/bun/extra/bundebug v1.2.7
github.com/vanng822/go-premailer v1.22.0 github.com/vanng822/go-premailer v1.22.0
github.com/wneessen/go-mail v0.6.2
) )
require ( require (
@ -65,14 +67,12 @@ require (
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
go.uber.org/multierr v1.11.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/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect
golang.org/x/net v0.33.0 // indirect golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.28.0 // indirect golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.23.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.67.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 gopkg.in/yaml.v3 v3.0.1 // indirect
mellium.im/sasl v0.3.2 // indirect mellium.im/sasl v0.3.2 // indirect
) )

29
go.sum
View file

@ -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 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 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.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.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 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 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= 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= 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.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 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= 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 h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE=
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= 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= 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/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 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 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= 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 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 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.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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= 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.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 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= 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= 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.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 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.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.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-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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/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.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.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.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-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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.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.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.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.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/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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= 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.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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.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.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.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-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.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 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.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 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= 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 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 h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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.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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View file

@ -12,10 +12,8 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
var ( // Logger is a configured logrus.Logger.
// Logger is a configured logrus.Logger. var Logger *logrus.Logger
Logger *logrus.Logger
)
// StructuredLogger is a structured logrus Logger. // StructuredLogger is a structured logrus Logger.
type StructuredLogger struct { type StructuredLogger struct {
@ -90,7 +88,7 @@ type StructuredLoggerEntry struct {
Logger logrus.FieldLogger 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{ l.Logger = l.Logger.WithFields(logrus.Fields{
"resp_status": status, "resp_status": status,
"resp_bytes_length": bytes, "resp_bytes_length": bytes,
@ -101,7 +99,7 @@ func (l *StructuredLoggerEntry) Write(status, bytes int, header http.Header, ela
} }
// Panic prints stack trace // 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{ l.Logger = l.Logger.WithFields(logrus.Fields{
"stack": string(stack), "stack": string(stack),
"panic": fmt.Sprintf("%+v", v), "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. // 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 { if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*StructuredLoggerEntry); ok {
entry.Logger = entry.Logger.WithField(key, value) entry.Logger = entry.Logger.WithField(key, value)
} }
} }
// LogEntrySetFields adds multiple fields to the request scoped logrus.FieldLogger. // 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 { if entry, ok := r.Context().Value(middleware.LogEntryCtxKey).(*StructuredLoggerEntry); ok {
entry.Logger = entry.Logger.WithFields(fields) entry.Logger = entry.Logger.WithFields(fields)
} }