Compare commits

..

No commits in common. "7018751d63eb231c33d62506e58f673e007fba8e" and "8c6fe34dc590bfec43a913db8d661fd97b985027" have entirely different histories.

20 changed files with 287 additions and 288 deletions

View file

@ -55,11 +55,9 @@ 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_ 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. 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 #### 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: 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,7 +47,6 @@ 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("/healthz", func(w http.ResponseWriter, _ *http.Request) { r.Get("/ping", func(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte("ok")) w.Write([]byte("pong"))
}) })
r.Get("/*", SPAHandler("public")) r.Get("/*", SPAHandler("public"))

View file

@ -2,7 +2,6 @@ package authorize
import ( import (
"net/http" "net/http"
"slices"
"github.com/go-chi/render" "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 { func hasRole(role string, roles []string) bool {
return slices.Contains(roles, role) for _, r := range roles {
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]any) error { func (c *AppClaims) ParseClaims(claims map[string]interface{}) 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]any) error {
var roles []string var roles []string
if rl != nil { if rl != nil {
for _, v := range rl.([]any) { for _, v := range rl.([]interface{}) {
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]any) error { func (c *RefreshClaims) ParseClaims(claims map[string]interface{}) 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 any) (map[string]any, error) { func ParseStructToMap(c interface{}) (map[string]interface{}, error) {
var claims map[string]any var claims map[string]interface{}
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,16 +33,21 @@ 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 email.Mailer Mailer Mailer
} }
// NewResource returns a configured authentication resource. // 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() loginAuth, err := NewLoginTokenAuth()
if err != nil { if err != nil {
return nil, err 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) tokenURL, _ := url.JoinPath(rs.LoginAuth.loginURL, lt.Token)
go func() { go func() {
content := ContentLoginToken{ content := email.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,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
@ -24,7 +25,7 @@ import (
var ( var (
auth *Resource auth *Resource
authStore MockAuthStore authStore MockAuthStore
mailer *email.MockMailer mailer email.MockMailer
ts *httptest.Server ts *httptest.Server
) )
@ -35,9 +36,7 @@ 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)
@ -74,6 +73,10 @@ 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
@ -103,11 +106,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.SendInvoked { if tc.err == nil && !mailer.LoginTokenInvoked {
t.Error("emailService.Send not invoked") t.Error("emailService.LoginToken not invoked")
} }
authStore.GetAccountByEmailInvoked = false 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() defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body) respBody, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
return nil, "" return nil, ""
@ -369,7 +372,7 @@ func genRefreshJWT(c jwt.RefreshClaims) string {
return tokenString return tokenString
} }
func encode(v any) (*bytes.Buffer, error) { func encode(v interface{}) (*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

View file

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

View file

@ -9,7 +9,9 @@ import (
"github.com/spf13/viper" "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. // 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]any Filter map[string]interface{}
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 any) (*AccountFilter, error) { func NewAccountFilter(params interface{}) (*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 any) (*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]any), Filter: make(map[string]interface{}),
Order: v["order"], Order: v["order"],
} }
// Parse limit and offset // Parse limit and offset

View file

@ -10,11 +10,15 @@ services:
LOG_LEVEL: debug LOG_LEVEL: debug
LOG_TEXTLOGGING: "true" LOG_TEXTLOGGING: "true"
#PORT: 3000 #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_EXPIRY: 1h
#AUTH_JWT_REFRESH_EXPIRY: 72h #AUTH_JWT_REFRESH_EXPIRY: 72h
#AUTH_JWT_SECRET: my secret #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_FROM_NAME: Go Base
#EMAIL_SMTP_HOST: #EMAIL_SMTP_HOST:
#EMAIL_SMTP_PORT: 465 #EMAIL_SMTP_PORT: 465
@ -23,7 +27,7 @@ services:
ENABLE_CORS: "true" ENABLE_CORS: "true"
postgres: postgres:
image: postgres:17-alpine image: postgres:16
restart: unless-stopped restart: unless-stopped
ports: ports:
- 5432:5432 - 5432:5432

29
email/auth.go Normal file
View file

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

170
email/email.go Normal file
View file

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

View file

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

View file

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

View file

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

18
go.mod
View file

@ -1,15 +1,14 @@
module github.com/dhax/go-base module github.com/dhax/go-base
go 1.23.0 go 1.23
toolchain go1.24.1
require ( 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/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
@ -23,7 +22,6 @@ 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 (
@ -67,12 +65,14 @@ 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.36.0 // indirect golang.org/x/crypto v0.31.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.38.0 // indirect golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.31.0 // indirect golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.23.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/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.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
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/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
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,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.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=
@ -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/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,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.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=
@ -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.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=
@ -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.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=
@ -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.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=
@ -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.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=
@ -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.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=
@ -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.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,8 +12,10 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// Logger is a configured logrus.Logger. var (
var Logger *logrus.Logger // Logger is a configured logrus.Logger.
Logger *logrus.Logger
)
// StructuredLogger is a structured logrus Logger. // StructuredLogger is a structured logrus Logger.
type StructuredLogger struct { type StructuredLogger struct {
@ -88,7 +90,7 @@ type StructuredLoggerEntry struct {
Logger logrus.FieldLogger 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{ l.Logger = l.Logger.WithFields(logrus.Fields{
"resp_status": status, "resp_status": status,
"resp_bytes_length": bytes, "resp_bytes_length": bytes,
@ -99,7 +101,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 any, stack []byte) { func (l *StructuredLoggerEntry) Panic(v interface{}, 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),
@ -116,14 +118,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 any) { func LogEntrySetField(r *http.Request, key string, value interface{}) {
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]any) { func LogEntrySetFields(r *http.Request, fields map[string]interface{}) {
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)
} }