diff options
| author | Bobby <[email protected]> | 2025-07-13 14:22:20 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2025-07-13 14:22:20 +0530 |
| commit | 3d7f8602d45583f25e2428bf6f8123453646dc08 (patch) | |
| tree | ecd707d298099ae9fda55efc3f0d1daf48f7b6e9 | |
| parent | bf112649d039f8f02e2135a74d8b506f7c31c784 (diff) | |
| download | imageboard-3d7f8602d45583f25e2428bf6f8123453646dc08.tar.xz imageboard-3d7f8602d45583f25e2428bf6f8123453646dc08.zip | |
registration controllers and email sending support
| -rw-r--r-- | config/types.go | 1 | ||||
| -rw-r--r-- | controllers/constants.go | 8 | ||||
| -rw-r--r-- | controllers/home.go | 4 | ||||
| -rw-r--r-- | controllers/login.go | 24 | ||||
| -rw-r--r-- | controllers/logout.go | 23 | ||||
| -rw-r--r-- | controllers/register.go | 75 | ||||
| -rw-r--r-- | database/database.go | 1 | ||||
| -rw-r--r-- | database/tokens.go | 65 | ||||
| -rw-r--r-- | database/user.go | 12 | ||||
| -rw-r--r-- | models/tokens.go | 34 | ||||
| -rw-r--r-- | models/user.go | 19 | ||||
| -rw-r--r-- | processors/processors.go | 1 | ||||
| -rw-r--r-- | processors/user.go | 43 | ||||
| -rw-r--r-- | router/routes.go | 12 | ||||
| -rw-r--r-- | session/session.go | 1 | ||||
| -rw-r--r-- | static/css/main.css | 9 | ||||
| -rw-r--r-- | templates/email/verification.html | 75 | ||||
| -rw-r--r-- | templates/register.django | 11 | ||||
| -rw-r--r-- | utils/auth/auth.go | 26 | ||||
| -rw-r--r-- | utils/email/email.go | 124 | ||||
| -rw-r--r-- | utils/validators/tokens.go | 14 |
21 files changed, 549 insertions, 33 deletions
diff --git a/config/types.go b/config/types.go index c8bdd75..129e3d2 100644 --- a/config/types.go +++ b/config/types.go @@ -6,6 +6,7 @@ type ServerConfig struct { Host string `env:"SERVER_HOST" default:"localhost"`
Port int `env:"SERVER_PORT" default:"8080"`
AppName string `env:"APP_NAME" default:"ImageBoard"`
+ AppBaseURL string `env:"APP_BASE_URL" default:"http://localhost:8080"`
AppSecret string `env:"APP_SECRET" default:"default_secret"`
IsDevMode bool `env:"DEV_MODE" default:"true"`
MinPasswordLength int `env:"MIN_PASSWORD_LENGTH" default:"8"`
diff --git a/controllers/constants.go b/controllers/constants.go index 95d75c1..021eeb9 100644 --- a/controllers/constants.go +++ b/controllers/constants.go @@ -32,6 +32,10 @@ const ( ERR_LOGIN_INVALID_CREDENTIALS = `The credentials you provided are incorrect. Did you <a href="` + URL_FORGOT_PASSWORD + `">forget your password</a>?` ERR_ACCOUNT_DISABLED = `Your account is disabled or banned. You can reach out to support for assistance.` ERR_ACCOUNT_UNABLE_TO_LOGIN = `You cannot log in at this time. Verify your email or contact support. If you misplaced your verification email, you can <a href="` + URL_RESEND_VERIFICATION + `">request a new one</a>.` - ERR_SESSION_FAILED_TO_CREATE = "Failed to create session. Please try again later." - ERR_SESSION_FAILED_TO_SAVE = "Failed to save session. Please try again later." + ERR_PASSWORD_MISMATCH = "Entered passwords do not match. Ensure both fields are identical." + ERR_SESSION_FAILED_TO_CREATE = "Server failed to create a session. If this issue persists, contact support." + ERR_SESSION_FAILED_TO_SAVE = "Server failed to save session data. If this issue persists, contact support." + + // Success messages + SUCCESS_USER_REGISTERED = "Your account has been created successfully. A verification email has been sent to your email address. You will only be able to log in after verifying your email. If you did not receive the email, you can <a href=\"" + URL_RESEND_VERIFICATION + "\">request a new one</a>." ) diff --git a/controllers/home.go b/controllers/home.go index 6758a4b..186ef24 100644 --- a/controllers/home.go +++ b/controllers/home.go @@ -7,6 +7,6 @@ import ( ) func HomePageController(ctx *fiber.Ctx) error { - ctx.Locals("Title", "Home Page") - return shortcuts.Render(ctx, "home", nil) + ctx.Locals("Title", PT_HOME) + return shortcuts.Render(ctx, TEMPLATE_HOME, nil) } diff --git a/controllers/login.go b/controllers/login.go index 12262e3..6eb4996 100644 --- a/controllers/login.go +++ b/controllers/login.go @@ -3,17 +3,15 @@ package controllers import ( "imageboard/database" "imageboard/session" + "imageboard/utils/auth" "imageboard/utils/shortcuts" "github.com/gofiber/fiber/v2" ) -func getRedirectURL(ctx *fiber.Ctx) string { - referer := ctx.Get("Referer") - if referer != "" && referer != ctx.BaseURL()+URL_LOGIN && referer != ctx.BaseURL()+URL_REGISTER { - return referer - } - return URL_HOME +type LoginForm struct { + Username string `json:"username" form:"username"` + Password string `json:"password" form:"password"` } func renderLoginError(ctx *fiber.Ctx, errorMsg string, statusCode int) error { @@ -25,11 +23,9 @@ func renderLoginError(ctx *fiber.Ctx, errorMsg string, statusCode int) error { func LoginPageController(ctx *fiber.Ctx) error { ctx.Locals("Title", PT_LOGIN) - sess, err := session.Store.Get(ctx) - if err == nil { - if userID, ok := sess.Get("user_id").(int); ok && userID != 0 { - return ctx.Redirect(getRedirectURL(ctx), fiber.StatusSeeOther) - } + + if auth.IsAuthenticated(ctx) { + return ctx.Redirect(auth.GetRedirectURL(ctx), fiber.StatusSeeOther) } return shortcuts.Render(ctx, TEMPLATE_LOGIN, nil) @@ -37,10 +33,6 @@ func LoginPageController(ctx *fiber.Ctx) error { func LoginPostController(ctx *fiber.Ctx) error { ctx.Locals("Title", PT_LOGIN) - type LoginForm struct { - Username string `json:"username" form:"username"` - Password string `json:"password" form:"password"` - } var form LoginForm var err error @@ -78,5 +70,5 @@ func LoginPostController(ctx *fiber.Ctx) error { user.UpdateLastUserLogin(database.DB) user.UpdateLastUserActivity(database.DB) - return ctx.Redirect(getRedirectURL(ctx), fiber.StatusSeeOther) + return ctx.Redirect(auth.GetRedirectURL(ctx), fiber.StatusSeeOther) } diff --git a/controllers/logout.go b/controllers/logout.go new file mode 100644 index 0000000..20c280d --- /dev/null +++ b/controllers/logout.go @@ -0,0 +1,23 @@ +package controllers + +import ( + "imageboard/session" + "imageboard/utils/auth" + + "github.com/gofiber/fiber/v2" +) + +func LogoutController(ctx *fiber.Ctx) error { + sess, err := session.Store.Get(ctx) + if err != nil { + return ctx.Redirect(auth.GetRedirectURL(ctx), fiber.StatusSeeOther) + } + + if err := sess.Destroy(); err != nil { + sess.Delete("user_id") + sess.Delete("username") + sess.Save() + } + + return ctx.Redirect(auth.GetRedirectURL(ctx), fiber.StatusSeeOther) +} diff --git a/controllers/register.go b/controllers/register.go index d16db9d..acadbc5 100644 --- a/controllers/register.go +++ b/controllers/register.go @@ -1,12 +1,83 @@ package controllers import ( + "imageboard/database" + "imageboard/models" + "imageboard/utils/auth" + "imageboard/utils/email" "imageboard/utils/shortcuts" + "log" + "strings" "github.com/gofiber/fiber/v2" ) +type RegisterForm struct { + Username string `json:"username" form:"username"` + Email string `json:"email" form:"email"` + Password string `json:"password" form:"password"` + ConfirmPassword string `json:"confirm_password" form:"confirm_password"` +} + +func renderRegisterError(ctx *fiber.Ctx, errorMsg string, statusCode int) error { + return shortcuts.RenderWithStatus(ctx, TEMPLATE_REGISTER, fiber.Map{ + "Error": errorMsg, + "Username": ctx.FormValue("username"), + "Email": ctx.FormValue("email"), + }, statusCode) +} + func RegisterPageController(ctx *fiber.Ctx) error { - ctx.Locals("Title", "Register") - return shortcuts.Render(ctx, "register", nil) + ctx.Locals("Title", PT_REGISTER) + + if auth.IsAuthenticated(ctx) { + return ctx.Redirect(auth.GetRedirectURL(ctx), fiber.StatusSeeOther) + } + + return shortcuts.Render(ctx, TEMPLATE_REGISTER, nil) +} + +func RegisterPostController(ctx *fiber.Ctx) error { + ctx.Locals("Title", PT_REGISTER) + + if auth.IsAuthenticated(ctx) { + return ctx.Redirect(auth.GetRedirectURL(ctx), fiber.StatusSeeOther) + } + + var form RegisterForm + if err := ctx.BodyParser(&form); err != nil { + return renderRegisterError(ctx, ERR_INVALID_FORM_DATA, fiber.StatusBadRequest) + } + + if form.Password != form.ConfirmPassword { + return renderRegisterError(ctx, ERR_PASSWORD_MISMATCH, fiber.StatusBadRequest) + } + + user := &models.User{ + Username: form.Username, + Email: form.Email, + Password: form.Password, + } + + if err := database.CreateUser(user); err != nil { + var statusCode int + if strings.Contains(err.Error(), "username") { + statusCode = fiber.StatusConflict + } else if strings.Contains(err.Error(), "email") { + statusCode = fiber.StatusBadRequest + } else { + statusCode = fiber.StatusInternalServerError + } + + return renderRegisterError(ctx, "Failed to create user: "+err.Error(), statusCode) + } + + if err := email.SendVerificationEmail(user); err != nil { + log.Printf("Failed to send verification email: %v", err) + return renderRegisterError(ctx, "User created but failed to send verification email", fiber.StatusInternalServerError) + } + + return shortcuts.Render(ctx, TEMPLATE_REGISTER, fiber.Map{ + "Success": SUCCESS_USER_REGISTERED, + }) } diff --git a/database/database.go b/database/database.go index d284c0f..dedae59 100644 --- a/database/database.go +++ b/database/database.go @@ -64,6 +64,7 @@ func autoMigrate() error { &models.ImageSize{}, &models.Tag{}, &models.Comment{}, + &models.EmailToken{}, ) } diff --git a/database/tokens.go b/database/tokens.go new file mode 100644 index 0000000..ddbc1f6 --- /dev/null +++ b/database/tokens.go @@ -0,0 +1,65 @@ +package database + +import ( + "fmt" + "imageboard/models" + "imageboard/utils/validators" + "time" +) + +func GenerateEmailToken(userID int, tokenType models.EmailTokenType) (*models.EmailToken, error) { + var existingToken models.EmailToken + if err := DB.Where("user_id = ? AND type = ?", userID, tokenType).First(&existingToken).Error; err == nil { + if err := DB.Delete(&existingToken).Error; err != nil { + return nil, err + } + } + + tokenValue, err := validators.GenerateRandomToken() + if err != nil { + return nil, err + } + + var expirationDuration time.Duration + switch tokenType { + case models.EmailTokenTypeVerification: + expirationDuration = 24 * time.Hour + case models.EmailTokenTypePasswordReset: + expirationDuration = 1 * time.Hour + case models.EmailTokenTypeChangeEmail: + expirationDuration = 1 * time.Hour + default: + expirationDuration = 1 * time.Hour + } + + token := &models.EmailToken{ + UserID: uint(userID), + Token: tokenValue, + Type: tokenType, + ExpiresAt: time.Now().Add(expirationDuration), + } + + if err := DB.Create(token).Error; err != nil { + return nil, err + } + + return token, nil +} + +func VerifyToken(userID int, token string, tokenType models.EmailTokenType) (*models.EmailToken, error) { + var emailToken models.EmailToken + if err := DB.Where("user_id = ? AND token = ? AND type = ?", userID, token, tokenType).First(&emailToken).Error; err != nil { + return nil, err + } + + if !emailToken.IsValid() { + return nil, fmt.Errorf("token is invalid or expired") + } + + emailToken.MarkAsUsed() + if err := DB.Save(&emailToken).Error; err != nil { + return nil, fmt.Errorf("failed to mark token as used: %w", err) + } + + return &emailToken, nil +} diff --git a/database/user.go b/database/user.go index c512038..4fe7e18 100644 --- a/database/user.go +++ b/database/user.go @@ -9,3 +9,15 @@ func GetUserByUsername(username string) (*models.User, error) { } return &user, nil } + +func GetUserByID(userID uint) (*models.User, error) { + var user models.User + if err := DB.Where("id = ?", userID).First(&user).Error; err != nil { + return nil, err + } + return &user, nil +} + +func CreateUser(user *models.User) error { + return DB.Create(user).Error +} diff --git a/models/tokens.go b/models/tokens.go new file mode 100644 index 0000000..c53ea2e --- /dev/null +++ b/models/tokens.go @@ -0,0 +1,34 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +type EmailToken struct { + gorm.Model + UserID uint `gorm:"not null;index" json:"user_id"` + Token string `gorm:"uniqueIndex;not null;size:64" json:"token"` + Type EmailTokenType `gorm:"not null;size:20" json:"type"` + ExpiresAt time.Time `gorm:"not null" json:"expires_at"` + UsedAt *time.Time `gorm:"default:null" json:"used_at"` + User User `gorm:"foreignKey:UserID" json:"user,omitempty"` +} + +func (et *EmailToken) IsExpired() bool { + return time.Now().After(et.ExpiresAt) +} + +func (et *EmailToken) IsUsed() bool { + return et.UsedAt != nil +} + +func (et *EmailToken) IsValid() bool { + return !et.IsExpired() && !et.IsUsed() +} + +func (et *EmailToken) MarkAsUsed() { + now := time.Now() + et.UsedAt = &now +} diff --git a/models/user.go b/models/user.go index 5607e68..3b92077 100644 --- a/models/user.go +++ b/models/user.go @@ -72,13 +72,27 @@ func (u *User) BeforeCreate(tx *gorm.DB) error { var userCount int64 if err := tx.Model(&User{}).Where("is_deleted = ?", false).Count(&userCount).Error; err != nil { - return err + return fmt.Errorf("failed to count existing users: %v", err) } if userCount == 0 { u.Level = UserLevelSuperAdmin // First user becomes Super Admin } + if len(u.Password) < config.Server.MinPasswordLength { + return fmt.Errorf("password must be at least %d characters long", config.Server.MinPasswordLength) + } + if len(u.Password) > 255 { + return fmt.Errorf("password must not exceed 255 characters") + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) + if err != nil { + return err + } + + u.Password = string(hashedPassword) + return nil } @@ -93,16 +107,13 @@ func (u *User) SetPassword(password string) error { if len(password) < config.Server.MinPasswordLength { return fmt.Errorf("password must be at least %d characters long", config.Server.MinPasswordLength) } - if len(password) > 255 { return fmt.Errorf("password must not exceed 255 characters") } - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return err } - u.Password = string(hashedPassword) return nil } diff --git a/processors/processors.go b/processors/processors.go index bbd23fe..db42480 100644 --- a/processors/processors.go +++ b/processors/processors.go @@ -4,6 +4,7 @@ import "github.com/gofiber/fiber/v2" func Initialize(app *fiber.App) {
app.Use(RequestContextProcessor)
+ app.Use(UserContextProcessor)
app.Use(MetaContextProcessor)
app.Use(SidebarContextProcessor)
app.Use(PreferencesContextProcessor)
diff --git a/processors/user.go b/processors/user.go new file mode 100644 index 0000000..b875c89 --- /dev/null +++ b/processors/user.go @@ -0,0 +1,43 @@ +package processors + +import ( + "imageboard/database" + "imageboard/models" + "imageboard/session" + + "github.com/gofiber/fiber/v2" +) + +func UserContextProcessor(ctx *fiber.Ctx) error { + var user *models.User + + sess, err := session.Store.Get(ctx) + if err == nil { + var userID uint + if id := sess.Get("user_id"); id != nil { + switch v := id.(type) { + case uint: + userID = v + case int: + userID = uint(v) + case int64: + userID = uint(v) + case float64: + userID = uint(v) + } + } + + if userID != 0 { + dbUser, err := database.GetUserByID(userID) + if err == nil && dbUser != nil { + dbUser.UpdateLastUserActivity(database.DB) + user = dbUser + } + } + } + + ctx.Locals("User", user) + ctx.Locals("IsAuthenticated", user != nil) + + return ctx.Next() +} diff --git a/router/routes.go b/router/routes.go index d72e990..c522599 100644 --- a/router/routes.go +++ b/router/routes.go @@ -9,8 +9,6 @@ import ( func Initialize(router *fiber.App) {
main := router.Group("/")
main.Get("/", controllers.HomePageController)
- main.Get("/register", controllers.RegisterPageController)
- main.Get("/preferences", controllers.PreferencesPageController)
posts := router.Group("/posts")
posts.Get("/", controllers.PostsController)
@@ -19,5 +17,15 @@ func Initialize(router *fiber.App) { login.Get("/", controllers.LoginPageController)
login.Post("/", controllers.LoginPostController)
+ logout := router.Group("/logout")
+ logout.Get("/", controllers.LogoutController)
+
+ register := router.Group("/register")
+ register.Get("/", controllers.RegisterPageController)
+ register.Post("/", controllers.RegisterPostController)
+
+ preferences := router.Group("/preferences")
+ preferences.Get("/", controllers.PreferencesPageController)
+
router.Use(controllers.NotFoundController)
}
diff --git a/session/session.go b/session/session.go index ed89c5a..ac99300 100644 --- a/session/session.go +++ b/session/session.go @@ -25,7 +25,6 @@ func init() { Storage: storage,
Expiration: config.Session.Expiration,
KeyLookup: "cookie:" + config.Session.CookieName,
- CookieName: config.Session.CookieName,
CookieDomain: config.Session.CookieDomain,
CookiePath: config.Session.CookiePath,
CookieSecure: config.Session.CookieSecure,
diff --git a/static/css/main.css b/static/css/main.css index af99d59..55b4fa1 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -363,4 +363,13 @@ footer::before { padding: 8px; margin-bottom: 16px; text-align: center; +} + +.success { + color: #ccffcc; + background-color: #003300; + border: 1px solid #00ff00; + padding: 8px; + margin-bottom: 16px; + text-align: center; }
\ No newline at end of file diff --git a/templates/email/verification.html b/templates/email/verification.html new file mode 100644 index 0000000..bef032c --- /dev/null +++ b/templates/email/verification.html @@ -0,0 +1,75 @@ +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Verify your email</title> + <style> + body { + background: #000033; + color: #ccccff; + font-family: monospace; + margin: 0; + padding: 0; + font-size: 13px; + text-align: justify; + } + .container { + background: #0d001a; + border: 1px solid #ff99cc; + max-width: 768px; + margin: 40px auto; + padding: 16px; + } + h1 { + color: #ffccff; + margin-bottom: 8px; + font-size: 16px; + } + p { + color: #99ffcc; + line-height: 18px; + } + .verify-link { + display: inline-block; + background: #330066; + color: #ccffcc; + border: 1px solid #99ffcc; + padding: 10px 24px; + text-decoration: none; + font-weight: bold; + margin: 12px auto; + } + .verify-link:hover { + background: #ff99cc; + color: #1a001a; + border-color: #ff99cc; + } + .footer { + color: #ffccff; + margin-top: 32px; + font-size: 11px; + } + </style> + </head> + <body> + <div class="container"> + <h1>Welcome to {{ .Appname }}, {{ .Username }}!</h1> + <p> + Your account has been created successfully. To complete your + registration, you need to verify your email address by clicking the link + below: + </p> + <a class="verify-link" href="{{ .Link }}">Verify Email</a> + <p> + If you are unable to click the link, copy and paste it into your + browser: + </p> + <p> + <a href="{{ .Link }}">{{ .Link }}</a> + </p> + <div class="footer"> + If you did not register for this account, you can ignore this email. + </div> + </div> + </body> +</html> diff --git a/templates/register.django b/templates/register.django index 02ba9d9..2990ba5 100644 --- a/templates/register.django +++ b/templates/register.django @@ -2,14 +2,17 @@ {% block content %} <div class="centered-main"> - <div class="bordered-box"> + <div class="bordered-box" style="max-width: 786px;"> <img src="/static/images/1c8fcc330ea1e971440cd3bdb8993a81.webp" alt="Register Image" class="q-img" /> <h1>Join {{ Appname }}</h1> <p>Create your account to start sharing and exploring images!</p> - {% if Error %} - <div class="error">{{ Error }}</div> - {% endif %} <form action="/register" method="POST" class="ibform"> + {% if Error %} + <div class="error">{{ Error|safe }}</div> + {% endif %} + {% if Success %} + <div class="success">{{ Success|safe }}</div> + {% endif %} <div class="fgroup"> <div class="fg-main"> <label for="username">Username</label> diff --git a/utils/auth/auth.go b/utils/auth/auth.go new file mode 100644 index 0000000..7b8f260 --- /dev/null +++ b/utils/auth/auth.go @@ -0,0 +1,26 @@ +package auth + +import ( + "imageboard/models" + + "github.com/gofiber/fiber/v2" +) + +func GetCurrentUser(ctx *fiber.Ctx) *models.User { + if user, ok := ctx.Locals("User").(*models.User); ok { + return user + } + return nil +} + +func IsAuthenticated(ctx *fiber.Ctx) bool { + return GetCurrentUser(ctx) != nil +} + +func GetRedirectURL(ctx *fiber.Ctx) string { + referer := ctx.Get("Referer") + if referer != "" && referer != ctx.BaseURL()+"/login" && referer != ctx.BaseURL()+"/register" { + return referer + } + return "/" +} diff --git a/utils/email/email.go b/utils/email/email.go new file mode 100644 index 0000000..fb1d58d --- /dev/null +++ b/utils/email/email.go @@ -0,0 +1,124 @@ +package email + +import ( + "bytes" + "fmt" + "html/template" + "imageboard/config" + "imageboard/database" + "imageboard/models" + "net/smtp" + "regexp" +) + +func extractEmailAddress(from string) string { + re := regexp.MustCompile(`<([^>]+)>`) + matches := re.FindStringSubmatch(from) + if len(matches) == 2 { + return matches[1] + } + return from +} + +func SendMail(to, subject, body string) error { + var auth smtp.Auth + if config.SMTP.Username != "" { + auth = smtp.PlainAuth("", config.SMTP.Username, config.SMTP.Password, config.SMTP.Host) + } else { + auth = nil + } + fromHeader := config.SMTP.From + fromAddress := extractEmailAddress(config.SMTP.From) + msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n%s", + fromHeader, to, subject, body) + addr := fmt.Sprintf("%s:%d", config.SMTP.Host, config.SMTP.Port) + return smtp.SendMail(addr, auth, fromAddress, []string{to}, []byte(msg)) +} + +func SendVerificationEmail(user *models.User) error { + token, err := database.GenerateEmailToken(int(user.ID), models.EmailTokenTypeVerification) + if err != nil { + return fmt.Errorf("failed to generate verification token: %w", err) + } + + tmpl, err := template.ParseFiles("templates/email/verification.html") + if err != nil { + return fmt.Errorf("failed to parse email template: %w", err) + } + verificationLink := fmt.Sprintf("%s/account/verify?token=%s", config.Server.AppBaseURL, token.Token) + data := struct { + Username string + Appname string + Link string + }{ + Username: user.Username, + Appname: config.Server.AppName, + Link: verificationLink, + } + + var body bytes.Buffer + if err := tmpl.Execute(&body, data); err != nil { + return fmt.Errorf("failed to execute email template: %w", err) + } + + subject := fmt.Sprintf("Verify your email for %s", config.Server.AppName) + return SendMail(user.Email, subject, body.String()) +} + +// func SendPasswordResetEmail(user *models.User) error { +// token, err := user.GenerateToken(database.DB, models.EmailTokenTypePasswordReset) +// if err != nil { +// return fmt.Errorf("failed to generate password reset token: %w", err) +// } + +// tmpl, err := template.ParseFiles("templates/email/password_reset.html") +// if err != nil { +// return fmt.Errorf("failed to parse email template: %w", err) +// } +// resetLink := fmt.Sprintf("%s/account/reset-password?token=%s", config.Server.AppBaseURL, token.Token) +// data := struct { +// Username string +// Link string +// }{ +// Username: user.Username, +// Link: resetLink, +// } + +// var body bytes.Buffer +// if err := tmpl.Execute(&body, data); err != nil { +// return fmt.Errorf("failed to execute email template: %w", err) +// } + +// subject := fmt.Sprintf("Password reset for %s", config.Server.AppName) +// return SendMail(user.Email, subject, body.String()) +// } + +// func SendEmailChangeConfirmation(user *models.User, newEmail string) error { +// token, err := user.GenerateToken(database.DB, models.EmailTokenTypeChangeEmail) +// if err != nil { +// return fmt.Errorf("failed to generate email change token: %w", err) +// } + +// tmpl, err := template.ParseFiles("templates/email/email_change.html") +// if err != nil { +// return fmt.Errorf("failed to parse email template: %w", err) +// } +// confirmLink := fmt.Sprintf("%s/account/confirm-email-change?token=%s&email=%s", config.Server.AppBaseURL, token.Token, newEmail) +// data := struct { +// Username string +// NewEmail string +// Link string +// }{ +// Username: user.Username, +// NewEmail: newEmail, +// Link: confirmLink, +// } + +// var body bytes.Buffer +// if err := tmpl.Execute(&body, data); err != nil { +// return fmt.Errorf("failed to execute email template: %w", err) +// } + +// subject := fmt.Sprintf("Confirm email change for %s", config.Server.AppName) +// return SendMail(newEmail, subject, body.String()) +// } diff --git a/utils/validators/tokens.go b/utils/validators/tokens.go new file mode 100644 index 0000000..f377c2e --- /dev/null +++ b/utils/validators/tokens.go @@ -0,0 +1,14 @@ +package validators + +import ( + "crypto/rand" + "encoding/hex" +) + +func GenerateRandomToken() (string, error) { + bytes := make([]byte, 32) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return hex.EncodeToString(bytes), nil +} |
