feat(notification): important events notifications (#4644)
This adds important event notifications.pull/4658/head
parent
1c3f650c72
commit
f685f247cf
|
@ -50,9 +50,15 @@ The following functions which mimic the behaviour of helm exist in most templati
|
||||||
- sha512sum
|
- sha512sum
|
||||||
- squote
|
- squote
|
||||||
- now
|
- now
|
||||||
|
- keys
|
||||||
|
- sortAlpha
|
||||||
|
- b64enc
|
||||||
|
- b64dec
|
||||||
|
- b32enc
|
||||||
|
- b32dec
|
||||||
|
|
||||||
See the [Helm Documentation](https://helm.sh/docs/chart_template_guide/function_list/) for more information. Please
|
See the [Helm Documentation](https://helm.sh/docs/chart_template_guide/function_list/) for more information. Please
|
||||||
note that only the functions listed above are supported.
|
note that only the functions listed above are supported and the functions don't necessarily behave exactly the same.
|
||||||
|
|
||||||
__*Special Note:* The `env` and `expandenv` function automatically excludes environment variables that start with
|
__*Special Note:* The `env` and `expandenv` function automatically excludes environment variables that start with
|
||||||
`AUTHELIA_` or `X_AUTHELIA_` and end with one of `KEY`, `SECRET`, `PASSWORD`, `TOKEN`, or `CERTIFICATE_CHAIN`.__
|
`AUTHELIA_` or `X_AUTHELIA_` and end with one of `KEY`, `SECRET`, `PASSWORD`, `TOKEN`, or `CERTIFICATE_CHAIN`.__
|
||||||
|
|
|
@ -46,8 +46,7 @@ func totpIdentityFinish(ctx *middlewares.AutheliaCtx, username string) {
|
||||||
ctx.Error(fmt.Errorf("unable to generate TOTP key: %s", err), messageUnableToRegisterOneTimePassword)
|
ctx.Error(fmt.Errorf("unable to generate TOTP key: %s", err), messageUnableToRegisterOneTimePassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ctx.Providers.StorageProvider.SaveTOTPConfiguration(ctx, *config)
|
if err = ctx.Providers.StorageProvider.SaveTOTPConfiguration(ctx, *config); err != nil {
|
||||||
if err != nil {
|
|
||||||
ctx.Error(fmt.Errorf("unable to save TOTP secret in DB: %s", err), messageUnableToRegisterOneTimePassword)
|
ctx.Error(fmt.Errorf("unable to save TOTP secret in DB: %s", err), messageUnableToRegisterOneTimePassword)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -57,10 +56,11 @@ func totpIdentityFinish(ctx *middlewares.AutheliaCtx, username string) {
|
||||||
Base32Secret: string(config.Secret),
|
Base32Secret: string(config.Secret),
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ctx.SetJSONBody(response)
|
if err = ctx.SetJSONBody(response); err != nil {
|
||||||
if err != nil {
|
|
||||||
ctx.Logger.Errorf("Unable to set TOTP key response in body: %s", err)
|
ctx.Logger.Errorf("Unable to set TOTP key response in body: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctxLogEvent(ctx, username, "Second Factor Method Added", map[string]any{"Action": "Second Factor Method Added", "Category": "Time-based One Time Password"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TOTPIdentityFinish the handler for finishing the identity validation.
|
// TOTPIdentityFinish the handler for finishing the identity validation.
|
||||||
|
|
|
@ -147,12 +147,10 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
userSession.Webauthn = nil
|
userSession.Webauthn = nil
|
||||||
if err = ctx.SaveSession(userSession); err != nil {
|
if err = ctx.SaveSession(userSession); err != nil {
|
||||||
ctx.Logger.Errorf(logFmtErrSessionSave, "removal of the attestation challenge", regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf(logFmtErrSessionSave, "removal of the attestation challenge", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.ReplyOK()
|
ctx.ReplyOK()
|
||||||
ctx.SetStatusCode(fasthttp.StatusCreated)
|
ctx.SetStatusCode(fasthttp.StatusCreated)
|
||||||
|
|
||||||
|
ctxLogEvent(ctx, userSession.Username, "Second Factor Method Added", map[string]any{"Action": "Second Factor Method Added", "Category": "Webauthn Credential", "Device Name": "Primary"})
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,10 +73,13 @@ func ResetPasswordPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data := templates.EmailPasswordResetValues{
|
data := templates.EmailEventValues{
|
||||||
Title: "Password changed successfully",
|
Title: "Password changed successfully",
|
||||||
DisplayName: userInfo.DisplayName,
|
DisplayName: userInfo.DisplayName,
|
||||||
RemoteIP: ctx.RemoteIP().String(),
|
RemoteIP: ctx.RemoteIP().String(),
|
||||||
|
Details: map[string]any{
|
||||||
|
"Action": "Password Reset",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
addresses := userInfo.Addresses()
|
addresses := userInfo.Addresses()
|
||||||
|
@ -84,7 +87,7 @@ func ResetPasswordPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
ctx.Logger.Debugf("Sending an email to user %s (%s) to inform that the password has changed.",
|
ctx.Logger.Debugf("Sending an email to user %s (%s) to inform that the password has changed.",
|
||||||
username, addresses[0])
|
username, addresses[0])
|
||||||
|
|
||||||
if err = ctx.Providers.Notifier.Send(ctx, addresses[0], "Password changed successfully", ctx.Providers.Templates.GetPasswordResetEmailTemplate(), data); err != nil {
|
if err = ctx.Providers.Notifier.Send(ctx, addresses[0], "Password changed successfully", ctx.Providers.Templates.GetEventEmailTemplate(), data); err != nil {
|
||||||
ctx.Logger.Error(err)
|
ctx.Logger.Error(err)
|
||||||
ctx.ReplyOK()
|
ctx.ReplyOK()
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,12 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/v4/internal/authentication"
|
||||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||||
|
"github.com/authelia/authelia/v4/internal/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
var bytesEmpty = []byte("")
|
var bytesEmpty = []byte("")
|
||||||
|
@ -24,3 +27,41 @@ func ctxGetPortalURL(ctx *middlewares.AutheliaCtx) (portalURL *url.URL) {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ctxLogEvent(ctx *middlewares.AutheliaCtx, username, description string, eventDetails map[string]any) {
|
||||||
|
var (
|
||||||
|
details *authentication.UserDetails
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx.Logger.Debugf("Getting user details for notification")
|
||||||
|
|
||||||
|
// Send Notification.
|
||||||
|
if details, err = ctx.Providers.UserProvider.GetDetails(username); err != nil {
|
||||||
|
ctx.Logger.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(details.Emails) == 0 {
|
||||||
|
ctx.Logger.Error(fmt.Errorf("user %s has no email address configured", username))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := templates.EmailEventValues{
|
||||||
|
Title: description,
|
||||||
|
DisplayName: details.DisplayName,
|
||||||
|
RemoteIP: ctx.RemoteIP().String(),
|
||||||
|
Details: eventDetails,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Logger.Debugf("Getting user addresses for notification")
|
||||||
|
|
||||||
|
addresses := details.Addresses()
|
||||||
|
|
||||||
|
ctx.Logger.Debugf("Sending an email to user %s (%s) to inform them of an important event.", username, addresses[0])
|
||||||
|
|
||||||
|
if err = ctx.Providers.Notifier.Send(ctx, addresses[0], description, ctx.Providers.Templates.GetEventEmailTemplate(), data); err != nil {
|
||||||
|
ctx.Logger.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
gomail "github.com/wneessen/go-mail"
|
"github.com/wneessen/go-mail"
|
||||||
"github.com/wneessen/go-mail/auth"
|
"github.com/wneessen/go-mail/auth"
|
||||||
|
|
||||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||||
|
@ -29,7 +29,7 @@ func NewOpportunisticSMTPAuth(config *schema.SMTPNotifierConfiguration) *Opportu
|
||||||
type OpportunisticSMTPAuth struct {
|
type OpportunisticSMTPAuth struct {
|
||||||
username, password, host string
|
username, password, host string
|
||||||
|
|
||||||
satPreference []gomail.SMTPAuthType
|
satPreference []mail.SMTPAuthType
|
||||||
sa smtp.Auth
|
sa smtp.Auth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,11 +43,11 @@ func (a *OpportunisticSMTPAuth) Start(server *smtp.ServerInfo) (proto string, to
|
||||||
for _, pref := range a.satPreference {
|
for _, pref := range a.satPreference {
|
||||||
if utils.IsStringInSlice(string(pref), server.Auth) {
|
if utils.IsStringInSlice(string(pref), server.Auth) {
|
||||||
switch pref {
|
switch pref {
|
||||||
case gomail.SMTPAuthPlain:
|
case mail.SMTPAuthPlain:
|
||||||
a.sa = smtp.PlainAuth("", a.username, a.password, a.host)
|
a.sa = smtp.PlainAuth("", a.username, a.password, a.host)
|
||||||
case gomail.SMTPAuthLogin:
|
case mail.SMTPAuthLogin:
|
||||||
a.sa = auth.LoginAuth(a.username, a.password, a.host)
|
a.sa = auth.LoginAuth(a.username, a.password, a.host)
|
||||||
case gomail.SMTPAuthCramMD5:
|
case mail.SMTPAuthCramMD5:
|
||||||
a.sa = smtp.CRAMMD5Auth(a.username, a.password)
|
a.sa = smtp.CRAMMD5Auth(a.username, a.password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,12 +57,12 @@ func (a *OpportunisticSMTPAuth) Start(server *smtp.ServerInfo) (proto string, to
|
||||||
|
|
||||||
if a.sa == nil {
|
if a.sa == nil {
|
||||||
for _, sa := range server.Auth {
|
for _, sa := range server.Auth {
|
||||||
switch gomail.SMTPAuthType(sa) {
|
switch mail.SMTPAuthType(sa) {
|
||||||
case gomail.SMTPAuthPlain:
|
case mail.SMTPAuthPlain:
|
||||||
a.sa = smtp.PlainAuth("", a.username, a.password, a.host)
|
a.sa = smtp.PlainAuth("", a.username, a.password, a.host)
|
||||||
case gomail.SMTPAuthLogin:
|
case mail.SMTPAuthLogin:
|
||||||
a.sa = auth.LoginAuth(a.username, a.password, a.host)
|
a.sa = auth.LoginAuth(a.username, a.password, a.host)
|
||||||
case gomail.SMTPAuthCramMD5:
|
case mail.SMTPAuthCramMD5:
|
||||||
a.sa = smtp.CRAMMD5Auth(a.username, a.password)
|
a.sa = smtp.CRAMMD5Auth(a.username, a.password)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ const (
|
||||||
const (
|
const (
|
||||||
TemplateNameEmailIdentityVerification = "IdentityVerification"
|
TemplateNameEmailIdentityVerification = "IdentityVerification"
|
||||||
TemplateNameEmailPasswordReset = "PasswordReset"
|
TemplateNameEmailPasswordReset = "PasswordReset"
|
||||||
|
TemplateNameEmailEvent = "Event"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Template Category Names.
|
// Template Category Names.
|
||||||
|
|
|
@ -4,11 +4,14 @@ import (
|
||||||
"crypto/sha1" //nolint:gosec
|
"crypto/sha1" //nolint:gosec
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
|
"encoding/base32"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -27,6 +30,8 @@ func FuncMap() map[string]any {
|
||||||
"hasPrefix": FuncStringHasPrefix,
|
"hasPrefix": FuncStringHasPrefix,
|
||||||
"hasSuffix": FuncStringHasSuffix,
|
"hasSuffix": FuncStringHasSuffix,
|
||||||
"lower": strings.ToLower,
|
"lower": strings.ToLower,
|
||||||
|
"keys": FuncKeys,
|
||||||
|
"sortAlpha": FuncSortAlpha,
|
||||||
"upper": strings.ToUpper,
|
"upper": strings.ToUpper,
|
||||||
"title": strings.ToTitle,
|
"title": strings.ToTitle,
|
||||||
"trim": strings.TrimSpace,
|
"trim": strings.TrimSpace,
|
||||||
|
@ -40,9 +45,43 @@ func FuncMap() map[string]any {
|
||||||
"sha512sum": FuncHashSum(sha512.New),
|
"sha512sum": FuncHashSum(sha512.New),
|
||||||
"squote": FuncStringSQuote,
|
"squote": FuncStringSQuote,
|
||||||
"now": time.Now,
|
"now": time.Now,
|
||||||
|
"b64enc": FuncB64Enc,
|
||||||
|
"b64dec": FuncB64Dec,
|
||||||
|
"b32enc": FuncB32Enc,
|
||||||
|
"b32dec": FuncB32Dec,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FuncB64Enc is a helper function that provides similar functionality to the helm b64enc func.
|
||||||
|
func FuncB64Enc(input string) string {
|
||||||
|
return base64.StdEncoding.EncodeToString([]byte(input))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuncB64Dec is a helper function that provides similar functionality to the helm b64dec func.
|
||||||
|
func FuncB64Dec(input string) (string, error) {
|
||||||
|
data, err := base64.StdEncoding.DecodeString(input)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuncB32Enc is a helper function that provides similar functionality to the helm b32enc func.
|
||||||
|
func FuncB32Enc(input string) string {
|
||||||
|
return base32.StdEncoding.EncodeToString([]byte(input))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuncB32Dec is a helper function that provides similar functionality to the helm b32dec func.
|
||||||
|
func FuncB32Dec(input string) (string, error) {
|
||||||
|
data, err := base32.StdEncoding.DecodeString(input)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
// FuncExpandEnv is a special version of os.ExpandEnv that excludes secret keys.
|
// FuncExpandEnv is a special version of os.ExpandEnv that excludes secret keys.
|
||||||
func FuncExpandEnv(s string) string {
|
func FuncExpandEnv(s string) string {
|
||||||
return os.Expand(s, FuncGetEnv)
|
return os.Expand(s, FuncGetEnv)
|
||||||
|
@ -68,6 +107,35 @@ func FuncHashSum(new func() hash.Hash) func(data string) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FuncKeys is a helper function that provides similar functionality to the helm keys func.
|
||||||
|
func FuncKeys(maps ...map[string]any) []string {
|
||||||
|
var keys []string
|
||||||
|
|
||||||
|
for _, m := range maps {
|
||||||
|
for k := range m {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuncSortAlpha is a helper function that provides similar functionality to the helm sortAlpha func.
|
||||||
|
func FuncSortAlpha(slice any) []string {
|
||||||
|
kind := reflect.Indirect(reflect.ValueOf(slice)).Kind()
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case reflect.Slice, reflect.Array:
|
||||||
|
unsorted := strslice(slice)
|
||||||
|
sorted := sort.StringSlice(unsorted)
|
||||||
|
sorted.Sort()
|
||||||
|
|
||||||
|
return sorted
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{strval(slice)}
|
||||||
|
}
|
||||||
|
|
||||||
// FuncStringReplace is a helper function that provides similar functionality to the helm replace func.
|
// FuncStringReplace is a helper function that provides similar functionality to the helm replace func.
|
||||||
func FuncStringReplace(old, new, s string) string {
|
func FuncStringReplace(old, new, s string) string {
|
||||||
return strings.ReplaceAll(s, old, new)
|
return strings.ReplaceAll(s, old, new)
|
||||||
|
@ -114,7 +182,7 @@ func FuncStringSQuote(in ...any) string {
|
||||||
|
|
||||||
for _, s := range in {
|
for _, s := range in {
|
||||||
if s != nil {
|
if s != nil {
|
||||||
out = append(out, fmt.Sprintf("%q", strval(s)))
|
out = append(out, fmt.Sprintf("'%s'", strval(s)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -325,3 +325,147 @@ func TestFuncStringSplitList(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFuncKeys(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
have []map[string]any
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{"ShouldProvideKeysSingle", []map[string]any{{"a": "v", "b": "v", "z": "v"}}, []string{"a", "b", "z"}},
|
||||||
|
{"ShouldProvideKeysMultiple", []map[string]any{{"a": "v", "b": "v", "z": "v"}, {"h": "v"}}, []string{"a", "b", "z", "h"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
keys := FuncKeys(tc.have...)
|
||||||
|
|
||||||
|
assert.Len(t, keys, len(tc.expected))
|
||||||
|
|
||||||
|
for _, expected := range tc.expected {
|
||||||
|
assert.Contains(t, keys, expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuncSortAlpha(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
have any
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{"ShouldSortStrings", []string{"a", "c", "b"}, []string{"a", "b", "c"}},
|
||||||
|
{"ShouldSortIntegers", []int{2, 3, 1}, []string{"1", "2", "3"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tc.expected, FuncSortAlpha(tc.have))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuncBEnc(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
have string
|
||||||
|
expected32 string
|
||||||
|
expected64 string
|
||||||
|
}{
|
||||||
|
{"ShouldEncodeEmptyString", "", "", ""},
|
||||||
|
{"ShouldEncodeString", "abc", "MFRGG===", "YWJj"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Run("Base32", func(t *testing.T) {
|
||||||
|
assert.Equal(t, tc.expected32, FuncB32Enc(tc.have))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Base64", func(t *testing.T) {
|
||||||
|
assert.Equal(t, tc.expected64, FuncB64Enc(tc.have))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuncBDec(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
have string
|
||||||
|
err32, expected32 string
|
||||||
|
err64, expected64 string
|
||||||
|
}{
|
||||||
|
{"ShouldDecodeEmptyString", "", "", "", "", ""},
|
||||||
|
{"ShouldDecodeBase32", "MFRGG===", "", "abc", "illegal base64 data at input byte 5", ""},
|
||||||
|
{"ShouldDecodeBase64", "YWJj", "illegal base32 data at input byte 3", "", "", "abc"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var (
|
||||||
|
actual string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Base32", func(t *testing.T) {
|
||||||
|
actual, err = FuncB32Dec(tc.have)
|
||||||
|
|
||||||
|
if tc.err32 != "" {
|
||||||
|
assert.Equal(t, "", actual)
|
||||||
|
assert.EqualError(t, err, tc.err32)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, tc.expected32, actual)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Base64", func(t *testing.T) {
|
||||||
|
actual, err = FuncB64Dec(tc.have)
|
||||||
|
|
||||||
|
if tc.err64 != "" {
|
||||||
|
assert.Equal(t, "", actual)
|
||||||
|
assert.EqualError(t, err, tc.err64)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, tc.expected64, actual)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuncStringQuote(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
have []any
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"ShouldQuoteSingleValue", []any{"abc"}, `"abc"`},
|
||||||
|
{"ShouldQuoteMultiValue", []any{"abc", 123}, `"abc" "123"`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tc.expected, FuncStringQuote(tc.have...))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuncStringSQuote(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
have []any
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"ShouldQuoteSingleValue", []any{"abc"}, `'abc'`},
|
||||||
|
{"ShouldQuoteMultiValue", []any{"abc", 123}, `'abc' '123'`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tc.expected, FuncStringSQuote(tc.have...))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -23,9 +23,8 @@ type Provider struct {
|
||||||
templates Templates
|
templates Templates
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPasswordResetEmailTemplate returns the EmailTemplate for Password Reset notifications.
|
func (p *Provider) GetEventEmailTemplate() (t *EmailTemplate) {
|
||||||
func (p *Provider) GetPasswordResetEmailTemplate() (t *EmailTemplate) {
|
return p.templates.notification.event
|
||||||
return p.templates.notification.passwordReset
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIdentityVerificationEmailTemplate returns the EmailTemplate for Identity Verification notifications.
|
// GetIdentityVerificationEmailTemplate returns the EmailTemplate for Identity Verification notifications.
|
||||||
|
@ -40,7 +39,7 @@ func (p *Provider) load() (err error) {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.templates.notification.passwordReset, err = loadEmailTemplate(TemplateNameEmailPasswordReset, p.config.EmailTemplatesPath); err != nil {
|
if p.templates.notification.event, err = loadEmailTemplate(TemplateNameEmailEvent, p.config.EmailTemplatesPath); err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,26 +74,28 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #ffffff;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
|
||||||
color: #0645AD;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
|
color: #ffffff;
|
||||||
padding: 15px 30px;
|
padding: 15px 30px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: rgb(25, 118, 210);
|
background: rgb(25, 118, 210);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: rgb(25, 118, 210);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*STYLES*/
|
/*STYLES*/
|
||||||
table[class=full] {
|
table[class=full] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -296,23 +298,39 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td style="font-family: Helvetica, arial, sans-serif; font-size: 16px; color: #333333; text-align:center; line-height: 30px;"
|
<td style="font-family: Helvetica, arial, sans-serif; font-size: 16px; color: #333333; text-align:center; line-height: 30px;"
|
||||||
st-title="fulltext-content">
|
st-title="fulltext-content">
|
||||||
Hi {{ .DisplayName }} <br/>
|
Hi {{ .DisplayName }}
|
||||||
Your password has been successfully reset.
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="font-family: Helvetica, arial, sans-serif; font-size: 16px; color: #333333; text-align:center; line-height: 30px;"
|
||||||
|
st-title="fulltext-content">
|
||||||
|
This email has been sent to you in order to notify you of an important event.
|
||||||
If you did not initiate the process your credentials might have been compromised. You should reset your password and contact an administrator.
|
If you did not initiate the process your credentials might have been compromised. You should reset your password and contact an administrator.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- End of Title -->
|
<!-- End of Title -->
|
||||||
|
<!-- spacing -->
|
||||||
|
<tr>
|
||||||
|
<td width="100%" height="20"
|
||||||
|
style="font-size:1px; line-height:1px; mso-line-height-rule: exactly;">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- End of spacing -->
|
||||||
|
<!-- content -->
|
||||||
|
{{- $keys := sortAlpha (keys .Details) }}
|
||||||
|
{{- range $key := $keys }}
|
||||||
|
<tr>
|
||||||
|
<td style="font-family: Helvetica, arial, sans-serif; font-size: 16px; color: #666666; text-align:center; line-height: 30px;"
|
||||||
|
st-content="fulltext-content">
|
||||||
|
<b>{{ $key }}:</b> {{ index $.Details $key }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{- end }}
|
||||||
|
<!-- End of content -->
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Spacing -->
|
|
||||||
<tr>
|
|
||||||
<td height="20"
|
|
||||||
style="font-size:1px; line-height:1px; mso-line-height-rule: exactly;">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<!-- Spacing -->
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
|
@ -1,7 +1,15 @@
|
||||||
Your password has been successfully reset.
|
This email has been sent to you in order to notify you of an important event.
|
||||||
|
|
||||||
If you did not initiate the process your credentials might have been compromised and you should reset your password and contact an administrator.
|
If you did not initiate the process your credentials might have been compromised and you should reset your password and contact an administrator.
|
||||||
|
|
||||||
|
{{- if ne (len .Details) 0 }}
|
||||||
|
{{- $keys := sortAlpha (keys .Details) }}
|
||||||
|
Details:
|
||||||
|
{{- range $key := $keys }}
|
||||||
|
{{ $key }}: {{ index $.Details $key }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
This email was generated by a user with the IP {{ .RemoteIP }}.
|
This email was generated by a user with the IP {{ .RemoteIP }}.
|
||||||
|
|
||||||
Please contact an administrator if you did not initiate this process.
|
Please contact an administrator if you did not initiate this process.
|
|
@ -13,8 +13,8 @@ type Templates struct {
|
||||||
|
|
||||||
// NotificationTemplates are the templates for the notification system.
|
// NotificationTemplates are the templates for the notification system.
|
||||||
type NotificationTemplates struct {
|
type NotificationTemplates struct {
|
||||||
passwordReset *EmailTemplate
|
|
||||||
identityVerification *EmailTemplate
|
identityVerification *EmailTemplate
|
||||||
|
event *EmailTemplate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Template covers shared implementations between the text and html template.Template.
|
// Template covers shared implementations between the text and html template.Template.
|
||||||
|
@ -36,9 +36,16 @@ type EmailTemplate struct {
|
||||||
Text *tt.Template
|
Text *tt.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EmailEventValues are the values used for event templates.
|
||||||
|
type EmailEventValues struct {
|
||||||
|
Title string
|
||||||
|
DisplayName string
|
||||||
|
Details map[string]any
|
||||||
|
RemoteIP string
|
||||||
|
}
|
||||||
|
|
||||||
// EmailPasswordResetValues are the values used for password reset templates.
|
// EmailPasswordResetValues are the values used for password reset templates.
|
||||||
type EmailPasswordResetValues struct {
|
type EmailPasswordResetValues struct {
|
||||||
UUID string
|
|
||||||
Title string
|
Title string
|
||||||
DisplayName string
|
DisplayName string
|
||||||
RemoteIP string
|
RemoteIP string
|
||||||
|
@ -46,7 +53,6 @@ type EmailPasswordResetValues struct {
|
||||||
|
|
||||||
// EmailIdentityVerificationValues are the values used for the identity verification templates.
|
// EmailIdentityVerificationValues are the values used for the identity verification templates.
|
||||||
type EmailIdentityVerificationValues struct {
|
type EmailIdentityVerificationValues struct {
|
||||||
UUID string
|
|
||||||
Title string
|
Title string
|
||||||
DisplayName string
|
DisplayName string
|
||||||
RemoteIP string
|
RemoteIP string
|
||||||
|
|
Loading…
Reference in New Issue