Compare commits
No commits in common. "7018751d63eb231c33d62506e58f673e007fba8e" and "8c6fe34dc590bfec43a913db8d661fd97b985027" have entirely different histories.
7018751d63
...
8c6fe34dc5
20 changed files with 287 additions and 288 deletions
11
README.md
11
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
29
email/auth.go
Normal 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
170
email/email.go
Normal 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
|
||||||
|
}
|
||||||
106
email/mailer.go
106
email/mailer.go
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
18
go.mod
|
|
@ -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
29
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 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=
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,10 @@ 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 {
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue