refactor: more logs, add english template

This commit is contained in:
Mysh! 2025-04-03 16:20:14 +02:00
parent 02a75b7d53
commit fda300e91d
No known key found for this signature in database
GPG key ID: 96739F8E0F9A0F2D
3 changed files with 375 additions and 113 deletions

398
main.go
View file

@ -18,6 +18,7 @@ package main
import ( import (
cryptorand "crypto/rand" cryptorand "crypto/rand"
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"github.com/akyoto/cache" "github.com/akyoto/cache"
"github.com/hugmouse/maddy-password-reset/templates" "github.com/hugmouse/maddy-password-reset/templates"
@ -37,37 +38,39 @@ import (
"time" "time"
) )
// ---------------------------------------------------------------
// Configuration
// Make sure to set these constants before running the program
// ---------------------------------------------------------------
const ( const (
// MaddyPath is path to your Maddy credentials database // MaddyPath is path to your Maddy credentials database
//
// FYI, Maddy's password database by default is "/var/lib/maddy/credentials.db" // FYI, Maddy's password database by default is "/var/lib/maddy/credentials.db"
MaddyPath = "" MaddyPath = "maddy.db" // MUST be set
// HostingURL is your domain name, // HostingURL is your domain name, including scheme and trailing slash,
// for example: `http://localhost:1323/` // for example: `http://localhost:1323/` or `https://example.com/`
HostingURL = "" HostingURL = "http://localhost:1323/" // MUST be set
// SMTPMailUsername is your full mail address, // SMTPMailUsername is your full mail address,
// for example: `robot@local.host` // for example: `robot@local.host`
SMTPMailUsername = "" SMTPMailUsername = "" // MUST be set (unless DebugBypassMailSending is true)
// SMTPMailPassword is your mailbox password // SMTPMailPassword is your mailbox password
SMTPMailPassword = "" SMTPMailPassword = "" // MUST be set (unless DebugBypassMailSending is true)
// SMTPMailHostname is your mail hostname, // SMTPMailHostname is your mail hostname,
// for example: `mx1.local.host` // for example: `mx1.local.host`
SMTPMailHostname = "" SMTPMailHostname = "" // MUST be set (unless DebugBypassMailSending is true)
// MXServer is your mail `MX` record + `PORT`, // MXServer is your mail `MX` record + `PORT`,
// for example: `mx1.local.host:587` // for example: `mx1.local.host:587`
MXServer = "" MXServer = "" // MUST be set (unless DebugBypassMailSending is true)
// EmailFrom is a EmailTemplate's "$FROM" section // EmailFrom is a EmailTemplate's "$FROM" section
EmailFrom = "" EmailFrom = "" // MUST be set (unless DebugBypassMailSending is true)
// EmailSubject is a EmailTemplate's "$SUBJECT" section // EmailSubject is a EmailTemplate's "$SUBJECT" section
EmailSubject = "" EmailSubject = "" // MUST be set (unless DebugBypassMailSending is true)
// EmailMessage is a EmailTemplate's "$MESSAGE" section // EmailMessage is a EmailTemplate's "$MESSAGE" section
//
// Remember to provide a password reset link to a user ($RESET_LINK) // Remember to provide a password reset link to a user ($RESET_LINK)
EmailMessage = "Here's your reset link: $RESET_LINK\r\n" EmailMessage = "Here's your reset link: $RESET_LINK\r\n"
// EmailTemplate is your reset mail message // EmailTemplate is your reset mail message
@ -84,27 +87,102 @@ const (
// HTTPServerPort is an HTTP server port // HTTPServerPort is an HTTP server port
HTTPServerPort = 1323 HTTPServerPort = 1323
// DebugBypassMailSending if true, will not send any emails and will print reset link to the console // DebugBypassMailSending - if true, skips SMTP checks and sending, logs reset link instead.
DebugBypassMailSending = true DebugBypassMailSending = true
) )
// ---------------------------------------------------------------
// Constants for random string generator
// ---------------------------------------------------------------
const ( const (
// TokenAlphabet is created for random string creation, see randomString() function TokenAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
TokenAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" RandomStringLength = 10
)
// ---------------------------------------------------------------
// Log Message Constants
// ---------------------------------------------------------------
const (
// Configuration Validation
logMsgConfigValidationStart = "[Startup] Validating configuration..."
logMsgConfigMaddyPathEmpty = "[Config] MaddyPath constant cannot be empty."
logMsgConfigHostingURLEmpty = "[Config] HostingURL constant cannot be empty."
logMsgConfigHostingURLSlashFmt = "[Config] HostingURL must end with a trailing slash '/'. Current value: %s"
logMsgConfigSmtpCheckStart = "[Config] Checking SMTP and Email Template configuration (DebugBypassMailSending is false)"
logMsgConfigSmtpUsernameEmpty = "[Config] SMTPMailUsername constant cannot be empty when DebugBypassMailSending is false."
logMsgConfigSmtpPasswordEmpty = "[Config] SMTPMailPassword constant cannot be empty when DebugBypassMailSending is false."
logMsgConfigSmtpHostnameEmpty = "[Config] SMTPMailHostname constant cannot be empty when DebugBypassMailSending is false."
logMsgConfigMxServerEmpty = "[Config] MXServer constant cannot be empty when DebugBypassMailSending is false."
logMsgConfigEmailFromEmpty = "[Config] EmailFrom constant cannot be empty when DebugBypassMailSending is false."
logMsgConfigEmailSubjectEmpty = "[Config] EmailSubject constant cannot be empty when DebugBypassMailSending is false."
logMsgConfigEmailMsgResetLinkCheck = "[EmailMessage const] Checking your template for $RESET_LINK"
logMsgConfigEmailMsgResetLinkMissing = "[EmailMessage const] Your message template does not contain $RESET_LINK, so user can't reset his password!"
logMsgConfigEmailTplPlaceholdersCheck = "[EmailTemplate const] Checking your template placeholders ($TO, $FROM, $SUBJECT, $MESSAGE)"
logMsgConfigEmailTplPlaceholderMissingFmt = "[EmailTemplate const] Your template does not contain %s, make sure to add it."
logMsgConfigSmtpCheckSkipped = "[Config] DebugBypassMailSending is true. Skipping SMTP and Email Template configuration checks."
logMsgConfigCacheTimeInvalid = "[Config] CacheTime must be a positive duration."
logMsgConfigHttpPortInvalid = "[Config] HTTPServerPort must be a valid port number (1-65535)."
// SMTP
logMsgSmtpAuthSetup = "[SMTP] Configuring SMTP authentication."
logMsgSmtpAuthSkipped = "[SMTP] Debug mode enabled, skipping SMTP authentication setup."
logMsgSmtpSendFailedFmt = "[SMTP] Failed to send password reset email to %q: %v"
logMsgSmtpSendSuccessFmt = "[SMTP] Password reset email successfully sent to %q"
logMsgSmtpDebugSendSkippedFmt = "[SMTP] Debug mode enabled. Would send reset email to %q."
logMsgSmtpDebugResetLinkFmt = "[SMTP] Reset link for %q: %s"
// Sqlite
logMsgDbLoadingFmt = "[Sqlite] Loading Maddy's credentials database from: %s"
logMsgDbOpenFailedFmt = "[Sqlite] Failed to open database: %v"
logMsgDbClosing = "[Sqlite] Closing database connection."
logMsgDbCloseErrorFmt = "[Sqlite] Error closing database: %v"
logMsgDbPingFailedFmt = "[Sqlite] Failed to connect to database: %v"
logMsgDbPingSuccess = "[Sqlite] Database connection successful."
logMsgDbUserNotFoundFmt = "[Sqlite] User email %q not found in Maddy database. No reset link generated."
logMsgDbQueryErrorFmt = "[Sqlite] Error querying Maddy database for user %q: %v"
// Cache
logMsgCacheRegisterFmt = "[Cache] Registering cache for password resets with expiry: %v"
logMsgCacheResetPendingFmt = "[Cache] Password reset request for %q already pending, ignoring new request."
// Echo server
logMsgEchoInit = "[Echo] Initializing echo web server"
logMsgEchoTplRegister = "[Echo] Registering Go templates"
logMsgEchoStartFmt = "[Echo] Starting Echo web server on %s"
logMsgEchoStartFailedFmt = "[Echo] Failed to start server: %v"
// Handlers
logMsgHandlerPostResetInvalidEmailFmt = "[Handler POST /reset] Invalid email address provided '%s': %v"
logMsgHandlerGetKeyEmpty = "[Handler GET /reset/:key] Received empty key."
logMsgHandlerGetKeyInvalidFmt = "[Handler GET /reset/:key] Invalid or expired key provided: %s"
logMsgHandlerPostKeyEmpty = "[Handler POST /reset/:key] Received empty key."
logMsgHandlerPostKeyEmptyPassword = "[Handler POST /reset/:key] Received empty password."
logMsgHandlerPostKeyInvalidFmt = "[Handler POST /reset/:key] Invalid or expired key submitted: %s"
logMsgHandlerPostKeyCacheTypeFmt = "[Handler POST /reset/:key] Value retrieved from cache for key %s is not a string: %T"
// Reset Process
logMsgResetTokenGeneratedFmt = "[Reset] Generated reset token %s for user %s. Link: %s"
// Maddy Command Execution
logMsgMaddyExecAttemptFmt = "[Maddy] Attempting to reset password for user %s via maddy command."
logMsgMaddyExecFailedFmt = "[Maddy] Failed to execute Maddy password reset command for %s: %v. Output: %s"
logMsgMaddyExecSuccessFmt = "[Maddy] Successfully reset password for user %s. Output: %s"
// Other
logMsgRandNumGenFailedCriticalFmt = "CRITICAL: Failed to generate random number: %v"
) )
func randomString(length int) string { func randomString(length int) string {
l := big.NewInt(int64(len(TokenAlphabet))) l := big.NewInt(int64(len(TokenAlphabet)))
res := new(strings.Builder) res := new(strings.Builder)
res.Grow(length)
for i := 0; i < length; i++ { for i := 0; i < length; i++ {
n, err := cryptorand.Int(cryptorand.Reader, l) n, err := cryptorand.Int(cryptorand.Reader, l)
if err != nil { if err != nil {
panic(err) log.Fatalf(logMsgRandNumGenFailedCriticalFmt, err)
} }
res.WriteByte(TokenAlphabet[n.Int64()]) res.WriteByte(TokenAlphabet[n.Int64()])
} }
return res.String() return res.String()
} }
@ -112,80 +190,132 @@ type Template struct {
templates *template.Template templates *template.Template
} }
func (t *Template) Render(w io.Writer, name string, data interface{}, _ echo.Context) error { func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data) return t.templates.ExecuteTemplate(w, name, data)
} }
func isValidEmailAddress(email string) error { func isValidEmailAddress(email string) error {
// Parse the email address using addressparser if email == "" {
mail, err := mail.ParseAddress(email) return errors.New("email address cannot be empty")
if err != nil {
return err
} }
// Check if the parsed address is not nil and has a valid email format addr, err := mail.ParseAddress(email)
if mail == nil || mail.Address == "" { if err != nil {
log.Println("[AddressParser]: Invalid Email Address: %v") return fmt.Errorf("invalid email format: %w", err)
return err }
if !strings.Contains(addr.Address, "@") {
return errors.New("invalid email format: missing @ symbol")
} }
return nil return nil
} }
func validateConfiguration() {
if MaddyPath == "" {
log.Fatalln(logMsgConfigMaddyPathEmpty)
}
if HostingURL == "" {
log.Fatalln(logMsgConfigHostingURLEmpty)
}
if !strings.HasSuffix(HostingURL, "/") {
log.Fatalf(logMsgConfigHostingURLSlashFmt, HostingURL)
}
if !DebugBypassMailSending {
log.Println(logMsgConfigSmtpCheckStart)
if SMTPMailUsername == "" {
log.Fatalln(logMsgConfigSmtpUsernameEmpty)
}
if SMTPMailPassword == "" {
log.Fatalln(logMsgConfigSmtpPasswordEmpty)
}
if SMTPMailHostname == "" {
log.Fatalln(logMsgConfigSmtpHostnameEmpty)
}
if MXServer == "" {
log.Fatalln(logMsgConfigMxServerEmpty)
}
if EmailFrom == "" {
log.Fatalln(logMsgConfigEmailFromEmpty)
}
if EmailSubject == "" {
log.Fatalln(logMsgConfigEmailSubjectEmpty)
}
log.Println(logMsgConfigEmailMsgResetLinkCheck)
if !strings.Contains(EmailMessage, "$RESET_LINK") {
log.Fatalln(logMsgConfigEmailMsgResetLinkMissing)
}
log.Println(logMsgConfigEmailTplPlaceholdersCheck)
requiredPlaceholders := []string{"$TO", "$FROM", "$SUBJECT", "$MESSAGE"}
for _, placeholder := range requiredPlaceholders {
if !strings.Contains(EmailTemplate, placeholder) {
log.Fatalf(logMsgConfigEmailTplPlaceholderMissingFmt, placeholder)
}
}
} else {
log.Println(logMsgConfigSmtpCheckSkipped)
}
if CacheTime <= 0 {
log.Fatalln(logMsgConfigCacheTimeInvalid)
}
if HTTPServerPort <= 0 || HTTPServerPort > 65535 {
log.Fatalln(logMsgConfigHttpPortInvalid)
}
}
func main() { func main() {
log.Println(logMsgConfigValidationStart)
validateConfiguration()
var auth smtp.Auth var auth smtp.Auth
if !DebugBypassMailSending { if !DebugBypassMailSending {
log.Println("[EmailMessage const] Checking your template") log.Println(logMsgSmtpAuthSetup)
if !strings.Contains(EmailMessage, "$RESET_LINK") {
log.Fatalln("[EmailMessage const] Your message template does not contain $RESET_LINK, so user can't reset his password!")
}
log.Println("[EmailTemplate const] Checking your template")
if !strings.Contains(EmailTemplate, "$TO") {
log.Fatalln("[EmailTemplate const] Your template does not contain $TO, make sure to add it.")
}
if !strings.Contains(EmailTemplate, "$FROM") {
log.Fatalln("[EmailTemplate const] Your template does not contain $FROM, make sure to add it.")
}
if !strings.Contains(EmailTemplate, "$SUBJECT") {
log.Fatalln("[EmailTemplate const] Your template does not contain $SUBJECT, make sure to add it, so user can see a message preview.")
}
if !strings.Contains(EmailTemplate, "$MESSAGE") {
log.Fatalln("[EmailTemplate const] Your template does not contain $MESSAGE, make sure to add it.")
}
// Set up authentication information.
auth = smtp.PlainAuth("", SMTPMailUsername, SMTPMailPassword, SMTPMailHostname) auth = smtp.PlainAuth("", SMTPMailUsername, SMTPMailPassword, SMTPMailHostname)
} else { } else {
log.Println("[SMTP] Debug mode enabled, not checking email template") log.Println(logMsgSmtpAuthSkipped)
} }
log.Println("[Sqlite] Loading Maddy's credentials database") log.Printf(logMsgDbLoadingFmt, MaddyPath)
db, err := sql.Open("sqlite", MaddyPath) db, err := sql.Open("sqlite", MaddyPath)
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalf(logMsgDbOpenFailedFmt, err)
} }
defer func() {
log.Println(logMsgDbClosing)
if err := db.Close(); err != nil {
log.Printf(logMsgDbCloseErrorFmt, err)
}
}()
log.Println("[Cache] Registering cache for password resets") if err := db.Ping(); err != nil {
log.Fatalf(logMsgDbPingFailedFmt, err)
}
log.Println(logMsgDbPingSuccess)
log.Printf(logMsgCacheRegisterFmt, CacheTime)
passwordResetCache := cache.New(CacheTime) passwordResetCache := cache.New(CacheTime)
log.Println("[Echo] Initializing echo web server") log.Println(logMsgEchoInit)
e := echo.New() e := echo.New()
e.HideBanner = true e.HideBanner = true
e.Use(middleware.LoggerWithConfig( e.Use(middleware.LoggerWithConfig(
middleware.LoggerConfig{ middleware.LoggerConfig{
Format: `${time_custom} [Echo] ${latency_human} ${method} ${uri} - Error = ${error} - ${remote_ip} "${user_agent}"` + "\n", Format: `${time_custom} [Echo] ${latency_human} ${method} ${uri} - Status=${status} Error="${error}" RemoteIP=${remote_ip} UserAgent="${user_agent}"` + "\n",
CustomTimeFormat: "2006/01/02 15:04:05", CustomTimeFormat: "2006/01/02 15:04:05",
})) }))
e.Use(middleware.Recover()) e.Use(middleware.Recover())
log.Println("[Echo] Registering Go templates") log.Println(logMsgEchoTplRegister)
t := template.Must(template.ParseFS(templates.Templates, "*.gohtml")) t := template.Must(template.ParseFS(templates.Templates, "*.gohtml"))
e.Renderer = &Template{ e.Renderer = &Template{
t, templates: t,
} }
e.GET("/reset", func(c echo.Context) error { e.GET("/reset", func(c echo.Context) error {
@ -193,89 +323,155 @@ func main() {
}) })
e.POST("/reset", func(c echo.Context) error { e.POST("/reset", func(c echo.Context) error {
mail := c.FormValue("email") email := c.FormValue("email")
err = isValidEmailAddress(mail) if err := isValidEmailAddress(email); err != nil {
if err != nil { log.Printf(logMsgHandlerPostResetInvalidEmailFmt, email, err)
log.Println("[AddressParser]: Invalid mail address: ", err) return c.Render(http.StatusBadRequest, "reset.gohtml", map[string]any{
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid mail address: %v", err)) "Error": "Invalid email address format provided.",
})
} }
go func() {
// Check if there is already a password reset go func(userEmail string) {
_, exists := passwordResetCache.Get(mail)
// Check if there is already a pending password reset for this email
_, exists := passwordResetCache.Get(userEmail)
if exists { if exists {
log.Printf("[Cache] Mail %q already exists in cache, ignoring\n", mail) log.Printf(logMsgCacheResetPendingFmt, userEmail)
return return
} }
// Check if it's exists in Maddy db // Check if user exists in Maddy DB
// It will return an error is there is no user found var dummy int
var password string dbErr := db.QueryRow("SELECT 1 FROM passwords WHERE key = ?", userEmail).Scan(&dummy)
err = db.QueryRow("SELECT value FROM passwords WHERE key = ?", mail).Scan(&password)
if err != nil { if dbErr != nil {
log.Println("[Sqlite] An error occurred while trying to get password from Maddy database:", err) if errors.Is(dbErr, sql.ErrNoRows) {
log.Printf(logMsgDbUserNotFoundFmt, userEmail)
} else {
log.Printf(logMsgDbQueryErrorFmt, userEmail, dbErr)
}
return return
} }
// Generating an unique key token := randomString(RandomStringLength)
random := randomString(10) passwordResetCache.Set(token, userEmail, CacheTime)
passwordResetCache.Set(random, mail, CacheTime) passwordResetCache.Set(userEmail, token, CacheTime)
// Connect to the server, authenticate, set the sender and recipient, resetLink := HostingURL + "reset/" + token
// and send the email all in one step. log.Printf(logMsgResetTokenGeneratedFmt, token, userEmail, resetLink)
to := []string{mail}
if !DebugBypassMailSending { if !DebugBypassMailSending {
msg := strings.ReplaceAll(EmailTemplate, "$TO", mail) msg := strings.ReplaceAll(EmailTemplate, "$TO", userEmail)
msg = strings.ReplaceAll(msg, "$FROM", EmailFrom) msg = strings.ReplaceAll(msg, "$FROM", EmailFrom)
msg = strings.ReplaceAll(msg, "$SUBJECT", EmailSubject) msg = strings.ReplaceAll(msg, "$SUBJECT", EmailSubject)
msg = strings.ReplaceAll(msg, "$MESSAGE", EmailMessage)
msg = strings.ReplaceAll(msg, "$RESET_LINK", HostingURL+"reset/"+random)
err := smtp.SendMail(MXServer, auth, SMTPMailUsername, to, []byte(msg)) messageBody := strings.ReplaceAll(EmailMessage, "$RESET_LINK", resetLink)
if err != nil { msg = strings.ReplaceAll(msg, "$MESSAGE", messageBody)
log.Println("[SMTP] Failed to send mail - ", err)
to := []string{userEmail}
smtpErr := smtp.SendMail(MXServer, auth, SMTPMailUsername, to, []byte(msg))
if smtpErr != nil {
log.Printf(logMsgSmtpSendFailedFmt, userEmail, smtpErr)
// Clean up cache if sending failed
passwordResetCache.Delete(token)
passwordResetCache.Delete(userEmail)
return return
} }
log.Printf(logMsgSmtpSendSuccessFmt, userEmail)
} else { } else {
log.Println("[SMTP] Debug mode enabled, not sending email") log.Printf(logMsgSmtpDebugSendSkippedFmt, userEmail)
log.Println("[SMTP] Reset link:", HostingURL+"reset/"+random) log.Printf(logMsgSmtpDebugResetLinkFmt, userEmail, resetLink)
} }
}() }(email)
// Always return success to the user to prevent email enumeration
return c.Render(http.StatusOK, "reset.gohtml", map[string]any{ return c.Render(http.StatusOK, "reset.gohtml", map[string]any{
"Sent": true, "Sent": true, // Indicates request received, not necessarily email sent successfully
}) })
}) })
e.GET("/reset/:key", func(c echo.Context) error { e.GET("/reset/:key", func(c echo.Context) error {
key := c.Param("key") key := c.Param("key")
_, exists := passwordResetCache.Get(key) if key == "" {
if !exists { log.Println(logMsgHandlerGetKeyEmpty)
return c.Redirect(http.StatusTemporaryRedirect, "/reset") return c.Redirect(http.StatusTemporaryRedirect, "/reset")
} }
_, exists := passwordResetCache.Get(key)
if !exists {
log.Printf(logMsgHandlerGetKeyInvalidFmt, key)
return c.Render(http.StatusNotFound, "reset.gohtml", map[string]any{
"Error": "This password reset link is invalid or has expired.",
})
}
return c.Render(http.StatusOK, "reset.gohtml", map[string]any{ return c.Render(http.StatusOK, "reset.gohtml", map[string]any{
"UniqueLinkTriggered": true, "UniqueLinkTriggered": true,
"Key": key,
}) })
}) })
e.POST("/reset/:key", func(c echo.Context) error { e.POST("/reset/:key", func(c echo.Context) error {
key := c.Param("key") key := c.Param("key")
password := c.FormValue("password") password := c.FormValue("password")
mail, exists := passwordResetCache.Get(key)
if exists { if key == "" {
log.Println(logMsgHandlerPostKeyEmpty)
return c.Render(http.StatusBadRequest, "reset.gohtml", map[string]any{
"Error": "Reset key is missing.",
})
}
if password == "" {
log.Println(logMsgHandlerPostKeyEmptyPassword)
return c.Render(http.StatusBadRequest, "reset.gohtml", map[string]any{
"UniqueLinkTriggered": true,
"Key": key,
"Error": "Password cannot be empty.",
})
}
emailVal, exists := passwordResetCache.Get(key)
if !exists {
log.Printf(logMsgHandlerPostKeyInvalidFmt, key)
return c.Render(http.StatusNotFound, "reset.gohtml", map[string]any{
"Error": "This password reset link is invalid or has expired. Please request a new one.",
})
}
email, ok := emailVal.(string)
if !ok {
log.Printf(logMsgHandlerPostKeyCacheTypeFmt, key, emailVal)
passwordResetCache.Delete(key) passwordResetCache.Delete(key)
passwordResetCache.Delete(email)
return c.Render(http.StatusInternalServerError, "reset.gohtml", map[string]any{
"Error": "An internal error occurred. Please try again.",
})
} }
maddyExecCommand := exec.Command("maddy", "creds", "password", "-p", password, mail.(string)) // Invalidate stuff
err = maddyExecCommand.Run() passwordResetCache.Delete(key)
if err != nil { passwordResetCache.Delete(email)
log.Println("[maddyExecCommand] Failed to execute Maddy's password reset command - ", err)
return err log.Printf(logMsgMaddyExecAttemptFmt, email)
maddyExecCommand := exec.Command("maddy", "creds", "password", "-p", password, email)
output, execErr := maddyExecCommand.CombinedOutput()
if execErr != nil {
log.Printf(logMsgMaddyExecFailedFmt, email, execErr, string(output))
return c.Render(http.StatusInternalServerError, "reset.gohtml", map[string]any{
"Error": "Failed to update password due to a server error.",
})
} }
return c.String(http.StatusOK, "All good! Your password is now changed.") log.Printf(logMsgMaddyExecSuccessFmt, email, string(output))
return c.Render(http.StatusOK, "reset.gohtml", map[string]any{
"Success": "Password successfully changed! You can now log in with your new password.",
})
}) })
log.Println("[echo] Starting Echo web server") serverAddr := ":" + strconv.Itoa(HTTPServerPort)
e.Logger.Fatal(e.Start(":" + strconv.Itoa(HTTPServerPort))) log.Printf(logMsgEchoStartFmt, serverAddr) // Use Printf for format string
if err := e.Start(serverAddr); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf(logMsgEchoStartFailedFmt, err)
}
} }

62
templates/reset.en.gohtml Normal file
View file

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Password Reset</title>
<style>
body {
padding: 1rem;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
input {
margin-top: 1rem;
padding: 1rem;
background: transparent;
border: 1px solid black;
color: black;
}
label {
display: block;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #222;
color: white;
}
input {
border: 1px solid #ffffff;
color: #ffffff;
}
}
</style>
</head>
<body>
<h1>Password Reset</h1>
{{ if .Error }}
{{ .Error }}
{{ else }}
{{ if .UniqueLinkTriggered }}
<p>Enter your new password below.</p>
<form action="" method="post">
<label for="password">Your new password</label>
<input type="password" name="password" id="password" placeholder="Enter your new password">
</form>
{{ else }}
{{ if .Sent }}
<p>A password reset email has been sent if that address exists.</p>
{{ else }}
<form action="/reset" method="post">
<label for="email">Email address</label>
<input type="email" name="email" id="email" placeholder="Enter your email address">
</form>
{{ end }}
{{ end }}
{{ end }}
</body>
</html>

View file

@ -38,20 +38,24 @@
</head> </head>
<body> <body>
<h1>Сброс пароля</h1> <h1>Сброс пароля</h1>
{{ if .UniqueLinkTriggered }} {{ if .Error }}
<p>Напишите здесь ваш новый пароль</p> {{ .Error }}
<form action="" method="post">
<label for="password">Ваш новый пароль</label>
<input type="password" name="password" id="password" placeholder="Введите свой новый пароль">
</form>
{{ else }} {{ else }}
{{ if .Sent }} {{ if .UniqueLinkTriggered }}
<p>Сообщение о сбросе пароля было отправлено, если такой адрес существует.</p> <p>Напишите здесь ваш новый пароль</p>
{{ else }} <form action="" method="post">
<form action="/reset" method="post"> <label for="password">Ваш новый пароль</label>
<label for="email">Email адрес</label> <input type="password" name="password" id="password" placeholder="Введите свой новый пароль">
<input type="email" name="email" id="email" placeholder="Введите свой email адрес">
</form> </form>
{{ else }}
{{ if .Sent }}
<p>Сообщение о сбросе пароля было отправлено, если такой адрес существует.</p>
{{ else }}
<form action="/reset" method="post">
<label for="email">Email адрес</label>
<input type="email" name="email" id="email" placeholder="Введите свой email адрес">
</form>
{{ end }}
{{ end }} {{ end }}
{{ end }} {{ end }}
</body> </body>