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
|
||||
- squote
|
||||
- now
|
||||
- keys
|
||||
- sortAlpha
|
||||
- b64enc
|
||||
- b64dec
|
||||
- b32enc
|
||||
- b32dec
|
||||
|
||||
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
|
||||
`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)
|
||||
}
|
||||
|
||||
err = ctx.Providers.StorageProvider.SaveTOTPConfiguration(ctx, *config)
|
||||
if err != nil {
|
||||
if err = ctx.Providers.StorageProvider.SaveTOTPConfiguration(ctx, *config); err != nil {
|
||||
ctx.Error(fmt.Errorf("unable to save TOTP secret in DB: %s", err), messageUnableToRegisterOneTimePassword)
|
||||
return
|
||||
}
|
||||
|
@ -57,10 +56,11 @@ func totpIdentityFinish(ctx *middlewares.AutheliaCtx, username string) {
|
|||
Base32Secret: string(config.Secret),
|
||||
}
|
||||
|
||||
err = ctx.SetJSONBody(response)
|
||||
if err != nil {
|
||||
if err = ctx.SetJSONBody(response); err != nil {
|
||||
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.
|
||||
|
|
|
@ -147,12 +147,10 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
|
|||
userSession.Webauthn = nil
|
||||
if err = ctx.SaveSession(userSession); err != nil {
|
||||
ctx.Logger.Errorf(logFmtErrSessionSave, "removal of the attestation challenge", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||
|
||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.ReplyOK()
|
||||
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
|
||||
}
|
||||
|
||||
data := templates.EmailPasswordResetValues{
|
||||
data := templates.EmailEventValues{
|
||||
Title: "Password changed successfully",
|
||||
DisplayName: userInfo.DisplayName,
|
||||
RemoteIP: ctx.RemoteIP().String(),
|
||||
Details: map[string]any{
|
||||
"Action": "Password Reset",
|
||||
},
|
||||
}
|
||||
|
||||
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.",
|
||||
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.ReplyOK()
|
||||
|
||||
|
|
|
@ -2,9 +2,12 @@ package handlers
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/authentication"
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/templates"
|
||||
)
|
||||
|
||||
var bytesEmpty = []byte("")
|
||||
|
@ -24,3 +27,41 @@ func ctxGetPortalURL(ctx *middlewares.AutheliaCtx) (portalURL *url.URL) {
|
|||
|
||||
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"
|
||||
"strings"
|
||||
|
||||
gomail "github.com/wneessen/go-mail"
|
||||
"github.com/wneessen/go-mail"
|
||||
"github.com/wneessen/go-mail/auth"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
|
@ -29,7 +29,7 @@ func NewOpportunisticSMTPAuth(config *schema.SMTPNotifierConfiguration) *Opportu
|
|||
type OpportunisticSMTPAuth struct {
|
||||
username, password, host string
|
||||
|
||||
satPreference []gomail.SMTPAuthType
|
||||
satPreference []mail.SMTPAuthType
|
||||
sa smtp.Auth
|
||||
}
|
||||
|
||||
|
@ -43,11 +43,11 @@ func (a *OpportunisticSMTPAuth) Start(server *smtp.ServerInfo) (proto string, to
|
|||
for _, pref := range a.satPreference {
|
||||
if utils.IsStringInSlice(string(pref), server.Auth) {
|
||||
switch pref {
|
||||
case gomail.SMTPAuthPlain:
|
||||
case mail.SMTPAuthPlain:
|
||||
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)
|
||||
case gomail.SMTPAuthCramMD5:
|
||||
case mail.SMTPAuthCramMD5:
|
||||
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 {
|
||||
for _, sa := range server.Auth {
|
||||
switch gomail.SMTPAuthType(sa) {
|
||||
case gomail.SMTPAuthPlain:
|
||||
switch mail.SMTPAuthType(sa) {
|
||||
case mail.SMTPAuthPlain:
|
||||
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)
|
||||
case gomail.SMTPAuthCramMD5:
|
||||
case mail.SMTPAuthCramMD5:
|
||||
a.sa = smtp.CRAMMD5Auth(a.username, a.password)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ const (
|
|||
const (
|
||||
TemplateNameEmailIdentityVerification = "IdentityVerification"
|
||||
TemplateNameEmailPasswordReset = "PasswordReset"
|
||||
TemplateNameEmailEvent = "Event"
|
||||
)
|
||||
|
||||
// Template Category Names.
|
||||
|
|
|
@ -4,11 +4,14 @@ import (
|
|||
"crypto/sha1" //nolint:gosec
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/base32"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -27,6 +30,8 @@ func FuncMap() map[string]any {
|
|||
"hasPrefix": FuncStringHasPrefix,
|
||||
"hasSuffix": FuncStringHasSuffix,
|
||||
"lower": strings.ToLower,
|
||||
"keys": FuncKeys,
|
||||
"sortAlpha": FuncSortAlpha,
|
||||
"upper": strings.ToUpper,
|
||||
"title": strings.ToTitle,
|
||||
"trim": strings.TrimSpace,
|
||||
|
@ -40,9 +45,43 @@ func FuncMap() map[string]any {
|
|||
"sha512sum": FuncHashSum(sha512.New),
|
||||
"squote": FuncStringSQuote,
|
||||
"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.
|
||||
func FuncExpandEnv(s string) string {
|
||||
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.
|
||||
func FuncStringReplace(old, new, s string) string {
|
||||
return strings.ReplaceAll(s, old, new)
|
||||
|
@ -114,7 +182,7 @@ func FuncStringSQuote(in ...any) string {
|
|||
|
||||
for _, s := range in {
|
||||
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
|
||||
}
|
||||
|
||||
// GetPasswordResetEmailTemplate returns the EmailTemplate for Password Reset notifications.
|
||||
func (p *Provider) GetPasswordResetEmailTemplate() (t *EmailTemplate) {
|
||||
return p.templates.notification.passwordReset
|
||||
func (p *Provider) GetEventEmailTemplate() (t *EmailTemplate) {
|
||||
return p.templates.notification.event
|
||||
}
|
||||
|
||||
// GetIdentityVerificationEmailTemplate returns the EmailTemplate for Identity Verification notifications.
|
||||
|
@ -40,7 +39,7 @@ func (p *Provider) load() (err error) {
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -74,26 +74,28 @@
|
|||
}
|
||||
|
||||
a {
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #0645AD;
|
||||
}
|
||||
|
||||
h1 {
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 15px 30px;
|
||||
border-radius: 10px;
|
||||
background: rgb(25, 118, 210);
|
||||
text-decoration: none;
|
||||
color: #ffffff;
|
||||
padding: 15px 30px;
|
||||
border-radius: 10px;
|
||||
background: rgb(25, 118, 210);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: rgb(25, 118, 210);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
/*STYLES*/
|
||||
table[class=full] {
|
||||
width: 100%;
|
||||
|
@ -296,23 +298,39 @@
|
|||
<tr>
|
||||
<td style="font-family: Helvetica, arial, sans-serif; font-size: 16px; color: #333333; text-align:center; line-height: 30px;"
|
||||
st-title="fulltext-content">
|
||||
Hi {{ .DisplayName }} <br/>
|
||||
Your password has been successfully reset.
|
||||
Hi {{ .DisplayName }}
|
||||
</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.
|
||||
</td>
|
||||
</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>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Spacing -->
|
||||
<tr>
|
||||
<td height="20"
|
||||
style="font-size:1px; line-height:1px; mso-line-height-rule: exactly;">
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Spacing -->
|
||||
</tbody>
|
||||
</table>
|
||||
</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 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 }}.
|
||||
|
||||
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.
|
||||
type NotificationTemplates struct {
|
||||
passwordReset *EmailTemplate
|
||||
identityVerification *EmailTemplate
|
||||
event *EmailTemplate
|
||||
}
|
||||
|
||||
// Template covers shared implementations between the text and html template.Template.
|
||||
|
@ -36,9 +36,16 @@ type EmailTemplate struct {
|
|||
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.
|
||||
type EmailPasswordResetValues struct {
|
||||
UUID string
|
||||
Title string
|
||||
DisplayName string
|
||||
RemoteIP string
|
||||
|
@ -46,7 +53,6 @@ type EmailPasswordResetValues struct {
|
|||
|
||||
// EmailIdentityVerificationValues are the values used for the identity verification templates.
|
||||
type EmailIdentityVerificationValues struct {
|
||||
UUID string
|
||||
Title string
|
||||
DisplayName string
|
||||
RemoteIP string
|
||||
|
|
Loading…
Reference in New Issue