From abd7e3a97f15fa5e28a521cc23b90a77dd5509ba Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Mon, 23 Mar 2026 22:35:30 +0100 Subject: [PATCH] Remove password reset functionality Backend: - Remove ForgotPassword API handler - Remove password reset token model and database table - Remove email service for sending reset emails - Remove SMTP configuration Frontend: - Remove /auth/forgot page - Remove forgot password link from login page - Remove forgot route from auth-only routes check --- backend/internal/api/handlers/auth.go | 51 +---- backend/internal/api/router.go | 5 +- backend/internal/config/config.go | 20 -- backend/internal/db/db.go | 29 +-- backend/internal/db/imc_users.go | 39 ---- backend/internal/mail/email.go | 190 ------------------- frontend/src/routes/+layout.svelte | 4 +- frontend/src/routes/auth/forgot/+page.svelte | 88 --------- frontend/src/routes/auth/login/+page.svelte | 4 - 9 files changed, 5 insertions(+), 425 deletions(-) delete mode 100644 backend/internal/mail/email.go delete mode 100644 frontend/src/routes/auth/forgot/+page.svelte diff --git a/backend/internal/api/handlers/auth.go b/backend/internal/api/handlers/auth.go index 5dab840..4f7b23d 100644 --- a/backend/internal/api/handlers/auth.go +++ b/backend/internal/api/handlers/auth.go @@ -1,7 +1,6 @@ package handlers import ( - "log" "net/http" "strings" "time" @@ -9,7 +8,6 @@ import ( "github.com/gin-gonic/gin" "github.com/imc-vibe/backend/internal/auth" "github.com/imc-vibe/backend/internal/db" - "github.com/imc-vibe/backend/internal/mail" ) const MaxLoginAttempts = 5 @@ -17,15 +15,13 @@ const MaxLoginAttempts = 5 type AuthHandler struct { db *db.DB jwtManager *auth.JWTManager - emailService *mail.EmailService trustedProxies []string } -func NewAuthHandler(database *db.DB, jwtManager *auth.JWTManager, emailService *mail.EmailService, trustedProxies []string) *AuthHandler { +func NewAuthHandler(database *db.DB, jwtManager *auth.JWTManager, trustedProxies []string) *AuthHandler { return &AuthHandler{ db: database, jwtManager: jwtManager, - emailService: emailService, trustedProxies: trustedProxies, } } @@ -40,10 +36,6 @@ type ChangePasswordRequest struct { NewPassword string `json:"newPassword" binding:"required,min=8"` } -type ForgotPasswordRequest struct { - Identifier string `json:"identifier" binding:"required"` -} - type UserResponse struct { ID uint `json:"id"` Username string `json:"username"` @@ -128,47 +120,6 @@ func (h *AuthHandler) Me(c *gin.Context) { }) } -func (h *AuthHandler) ForgotPassword(c *gin.Context) { - var req ForgotPasswordRequest - if err := c.ShouldBindJSON(&req); err != nil { - Error(c, http.StatusBadRequest, "invalid request") - return - } - - req.Identifier = strings.TrimSpace(strings.ToLower(req.Identifier)) - if req.Identifier == "" { - Error(c, http.StatusBadRequest, "identifier required") - return - } - - user, err := h.db.GetImcUserByUsername(req.Identifier) - if err != nil { - Success(c, map[string]string{ - "message": "If the account exists, a password reset link will be sent", - }) - return - } - - token := mail.GenerateToken() - expiresAt := time.Now().Add(1 * time.Hour) - - if err := h.db.CreatePasswordResetToken(user.ID, token, expiresAt); err != nil { - log.Printf("Failed to create password reset token: %v", err) - Error(c, http.StatusInternalServerError, "failed to create reset token") - return - } - - if err := h.emailService.SendPasswordReset(user.Username, token); err != nil { - log.Printf("Failed to send password reset email: %v", err) - Error(c, http.StatusInternalServerError, "failed to send email") - return - } - - Success(c, map[string]string{ - "message": "If the account exists, a password reset link will be sent", - }) -} - func (h *AuthHandler) ChangePassword(c *gin.Context) { authCtx := GetAuthContext(c) if authCtx == nil { diff --git a/backend/internal/api/router.go b/backend/internal/api/router.go index 1756d22..32d9c6d 100644 --- a/backend/internal/api/router.go +++ b/backend/internal/api/router.go @@ -8,7 +8,6 @@ import ( "github.com/imc-vibe/backend/internal/auth" "github.com/imc-vibe/backend/internal/config" "github.com/imc-vibe/backend/internal/db" - "github.com/imc-vibe/backend/internal/mail" ) type Router struct { @@ -24,10 +23,9 @@ type Router struct { func New(database *db.DB, cfg *config.Config) *Router { jwtManager := auth.NewJWTManager(cfg.JWTSecret, "imc-vibe") - emailService := mail.NewEmailService(cfg) r := &Router{ - authHandler: handlers.NewAuthHandler(database, jwtManager, emailService, cfg.TrustedProxies), + authHandler: handlers.NewAuthHandler(database, jwtManager, cfg.TrustedProxies), domainHandler: handlers.NewDomainHandler(database), userHandler: handlers.NewUserHandler(database), aliasHandler: handlers.NewAliasHandler(database), @@ -46,7 +44,6 @@ func (r *Router) RegisterRoutes(engine *gin.Engine) { }) engine.POST("/api/auth/login", r.authHandler.Login) - engine.POST("/api/auth/forgot", r.authHandler.ForgotPassword) engine.POST("/api/auth/logout", r.authHandler.Logout) authGroup := engine.Group("/api") diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index bb2268b..a499ad7 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -39,16 +39,6 @@ type Config struct { // Rspamd settings (spam filter) RspamdAPI string // URL of the Rspamd web interface - - // SMTP settings (for sending password reset emails) - SMTPHost string // SMTP server hostname - SMTPPort string // SMTP server port (587 for submission, 465 for SMTPS) - SMTPUser string // SMTP username - SMTPPassword string // SMTP password - SMTPFrom string // From address in outgoing emails - - // Application settings - BaseURL string // Base URL of this application (used for generating links in emails) } // Load reads configuration from environment variables and .env files. @@ -98,16 +88,6 @@ func Load() *Config { // External services RspamdAPI: getEnv("RSPAMD_API", "http://127.0.0.1:11334"), - // SMTP settings (for password reset emails) - SMTPHost: getEnv("SMTP_HOST", "localhost"), - SMTPPort: getEnv("SMTP_PORT", "587"), - SMTPUser: getEnv("SMTP_USER", ""), - SMTPPassword: getEnv("SMTP_PASSWORD", ""), - SMTPFrom: getEnv("SMTP_FROM", "noreply@localhost"), - - // Application URL (for generating links in emails) - BaseURL: getEnv("BASE_URL", "http://localhost:8080"), - // Trusted proxies (comma-separated IPs) TrustedProxies: parseTrustedProxies(getEnv("TRUSTED_PROXIES", "")), } diff --git a/backend/internal/db/db.go b/backend/internal/db/db.go index 0106612..335e593 100644 --- a/backend/internal/db/db.go +++ b/backend/internal/db/db.go @@ -113,19 +113,6 @@ type ImcLoginAttempt struct { // TableName specifies the database table name for ImcLoginAttempt. func (ImcLoginAttempt) TableName() string { return "imc_login_attempts" } -// PasswordResetToken stores one-time tokens for password reset functionality. -type PasswordResetToken struct { - ID uint `gorm:"primaryKey" json:"id"` // Primary key - UserID uint `gorm:"not null" json:"userId"` // Foreign key to ImcUser - Token string `gorm:"size:64;not null;uniqueIndex" json:"token"` // The reset token (unique index for fast lookup) - ExpiresAt time.Time `gorm:"not null" json:"expiresAt"` // When this token expires - Used bool `gorm:"default:false" json:"used"` // Whether this token has been used - CreatedAt time.Time `json:"createdAt"` // When the token was created -} - -// TableName specifies the database table name for PasswordResetToken. -func (PasswordResetToken) TableName() string { return "imc_password_reset_tokens" } - // ============================================================================= // Helper/View Models (not stored in database) // ============================================================================= @@ -258,19 +245,5 @@ func (d *DB) InitSchema() error { return err } - // Create imc_password_reset_tokens table for password reset functionality. - // Stores temporary tokens for resetting forgotten passwords. - resetTokensSQL := ` - CREATE TABLE IF NOT EXISTS imc_password_reset_tokens ( - id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - user_id INT UNSIGNED NOT NULL, - token VARCHAR(64) NOT NULL UNIQUE, - expires_at DATETIME NOT NULL, - used BOOLEAN DEFAULT FALSE, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - INDEX idx_token (token), - INDEX idx_user_id (user_id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 - ` - return d.Exec(resetTokensSQL).Error + return nil } diff --git a/backend/internal/db/imc_users.go b/backend/internal/db/imc_users.go index a02c837..0fb7213 100644 --- a/backend/internal/db/imc_users.go +++ b/backend/internal/db/imc_users.go @@ -1,7 +1,5 @@ package db -import "time" - // ============================================================================= // Admin User (ImcUser) Operations // These functions manage admin users who can log into the web interface. @@ -189,40 +187,3 @@ func (d *DB) IsUserInDomain(userID, domainID uint) (bool, error) { err := d.Model(&ImcUserDomain{}).Where("user_id = ? AND domain_id = ?", userID, domainID).Count(&count).Error return count > 0, err } - -// ============================================================================= -// Password Reset Tokens -// These functions manage one-time tokens for password reset. -// ============================================================================= - -// CreatePasswordResetToken creates a new password reset token. -// userID: the ID of the user requesting password reset. -// token: the random token string. -// expiresAt: when this token becomes invalid. -func (d *DB) CreatePasswordResetToken(userID uint, token string, expiresAt time.Time) error { - resetToken := PasswordResetToken{ - UserID: userID, - Token: token, - ExpiresAt: expiresAt, - } - return d.Create(&resetToken).Error -} - -// GetValidPasswordResetToken looks up a valid (unused, not expired) reset token. -// token: the token string to look up. -// Returns: the token if valid, or an error if not found/expired/used. -func (d *DB) GetValidPasswordResetToken(token string) (*PasswordResetToken, error) { - var resetToken PasswordResetToken - // Check token exists, hasn't been used, and hasn't expired. - err := d.Where("token = ? AND used = false AND expires_at > ?", token, time.Now()).First(&resetToken).Error - if err != nil { - return nil, err - } - return &resetToken, nil -} - -// MarkResetTokenUsed marks a token as used (prevents reuse). -// token: the token string to mark as used. -func (d *DB) MarkResetTokenUsed(token string) error { - return d.Model(&PasswordResetToken{}).Where("token = ?", token).Update("used", true).Error -} diff --git a/backend/internal/mail/email.go b/backend/internal/mail/email.go deleted file mode 100644 index 64e0f45..0000000 --- a/backend/internal/mail/email.go +++ /dev/null @@ -1,190 +0,0 @@ -// Package mail provides email sending functionality for password reset emails. -// Uses Go's net/smtp package for sending emails via SMTP protocol. -package mail - -import ( - "crypto/rand" // cryptographically secure random number generator - "crypto/tls" // TLS/SSL encryption for secure email sending - "encoding/hex" // hex encoding for token generation - "fmt" // string formatting for URLs and messages - "net/smtp" // simple mail transfer protocol package - - "github.com/imc-vibe/backend/internal/config" -) - -// EmailService handles sending emails through SMTP. -// Stores SMTP configuration and provides methods for different email types. -type EmailService struct { - host string // SMTP server hostname (e.g., "smtp.example.com") - port string // SMTP server port (e.g., "587" for TLS, "465" for SSL) - username string // SMTP authentication username - password string // SMTP authentication password - from string // Sender email address (e.g., "noreply@example.com") - baseURL string // Base URL of the web application (for building reset links) -} - -// NewEmailService creates a new EmailService instance from configuration. -// cfg: application configuration containing SMTP settings and base URL. -func NewEmailService(cfg *config.Config) *EmailService { - return &EmailService{ - host: cfg.SMTPHost, - port: cfg.SMTPPort, - username: cfg.SMTPUser, - password: cfg.SMTPPassword, - from: cfg.SMTPFrom, - baseURL: cfg.BaseURL, - } -} - -// SendPasswordReset sends a password reset email to the user. -// to: recipient email address -// token: unique reset token that was generated and stored in the database -// The email contains a clickable link to reset the password. -func (s *EmailService) SendPasswordReset(to, token string) error { - // Build the reset URL with the token as a query parameter. - // Example: https://mail.example.com/auth/reset-password?token=abc123... - resetURL := fmt.Sprintf("%s/auth/reset-password?token=%s", s.baseURL, token) - - // Compose the email subject and body. - subject := "Password Reset Request" - body := fmt.Sprintf(`Hi, - -You requested a password reset for your account. - -Click the link below to reset your password: -%s - -This link will expire in 1 hour. - -If you didn't request this, please ignore this email. - -Best regards, -IMC Vibe Mail Server Admin -`, resetURL) - - // Send the email using the send method. - return s.send(to, subject, body) -} - -// send is the internal method that actually sends the email via SMTP. -// Handles both port 587 (TLS) and port 465 (SSL) connections. -func (s *EmailService) send(to, subject, body string) error { - // Check if SMTP is configured. If not, return an error. - if s.host == "" || s.username == "" { - return fmt.Errorf("SMTP not configured") - } - - // Build the server address in host:port format. - addr := fmt.Sprintf("%s:%s", s.host, s.port) - - // Build the email message with headers. - // \r\n is the standard line ending for email headers and body. - msg := fmt.Sprintf("From: %s\r\n"+ - "To: %s\r\n"+ - "Subject: %s\r\n"+ - "MIME-Version: 1.0\r\n"+ - "Content-Type: text/plain; charset=\"UTF-8\"\r\n"+ - "\r\n"+ - "%s", s.from, to, subject, body) - - // Port 465 uses implicit SSL/TLS, port 587 uses STARTTLS. - // Both are secure, but they work differently. - var auth smtp.Auth - if s.port == "465" { - // For port 465, we need to establish a TLS connection first - // before sending anything, then upgrade the connection. - err := s.sendWithTLS(addr, to, msg) - return err - } else { - // For other ports (usually 587), use STARTTLS which upgrades - // a plain connection to TLS after connecting. - auth = smtp.PlainAuth("", s.username, s.password, s.host) - err := smtp.SendMail(addr, auth, s.from, []string{to}, []byte(msg)) - return err - } -} - -// sendWithTLS sends an email using an explicit TLS connection on port 465. -// Port 465 uses implicit TLS - the entire connection is encrypted from the start. -func (s *EmailService) sendWithTLS(addr, to, msg string) error { - // Create a TLS configuration for the secure connection. - // ServerName must match the SMTP server's certificate. - tlsConfig := &tls.Config{ - ServerName: s.host, - } - - // Establish a TLS connection to the SMTP server. - // This creates an encrypted tunnel from the start. - conn, err := tls.Dial("tcp", addr, tlsConfig) - if err != nil { - return err - } - defer conn.Close() // Ensure connection is closed when we're done - - // Create an SMTP client on top of the TLS connection. - client, err := smtp.NewClient(conn, s.host) - if err != nil { - return err - } - defer client.Close() - - // Authenticate with the SMTP server using PLAIN authentication. - // The empty string "" is the identity (usually same as username). - auth := smtp.PlainAuth("", s.username, s.password, s.host) - if err := client.Auth(auth); err != nil { - return err - } - - // Set the sender (MAIL FROM command). - if err := client.Mail(s.from); err != nil { - return err - } - - // Set the recipient (RCPT TO command). - if err := client.Rcpt(to); err != nil { - return err - } - - // Start sending the message body (DATA command). - w, err := client.Data() - if err != nil { - return err - } - - // Write the email body to the data stream. - _, err = w.Write([]byte(msg)) - if err != nil { - return err - } - - // Close the data writer to finish the message. - err = w.Close() - if err != nil { - return err - } - - // Send the QUIT command to gracefully close the connection. - return client.Quit() -} - -// GenerateToken creates a cryptographically random token for password reset links. -// Returns a 64-character hexadecimal string (32 bytes encoded as hex). -// Uses crypto/rand for security-sensitive token generation. -func GenerateToken() string { - b := make([]byte, 32) - if _, err := rand.Read(b); err != nil { - panic("crypto/rand failed: " + err.Error()) - } - return hex.EncodeToString(b) -} - -// ValidateToken checks if a token has the correct format. -// Returns true if the token is exactly 64 characters of valid hex. -// This is a quick validation before checking the database. -func ValidateToken(token string) bool { - if len(token) != 64 { - return false - } - _, err := hex.DecodeString(token) - return err == nil -} diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index ee04475..faa8b57 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -31,9 +31,9 @@ $effect(() => { const path = $page.url.pathname; - // Auth-only routes (login, forgot) - no auth required, no layout chrome + // Auth-only routes (login) - no auth required, no layout chrome // /auth/change-password requires auth, so treat it like a regular route - const isAuthOnlyRoute = path === '/auth/login' || path === '/auth/forgot'; + const isAuthOnlyRoute = path === '/auth/login'; if (isAuthOnlyRoute) { loading = false; } else { diff --git a/frontend/src/routes/auth/forgot/+page.svelte b/frontend/src/routes/auth/forgot/+page.svelte deleted file mode 100644 index 3475a65..0000000 --- a/frontend/src/routes/auth/forgot/+page.svelte +++ /dev/null @@ -1,88 +0,0 @@ - - -
-
-
-

Reset Password

- - {#if error} -
- {error} -
- {/if} - - {#if success} -
- - - - If an account exists with that email or username, a password reset link has been sent. -
- - {:else} -

Enter your email address or username and we'll send you a link to reset your password.

- -
-
- Email or Username - -
- - -
- - - {/if} -
-
-
diff --git a/frontend/src/routes/auth/login/+page.svelte b/frontend/src/routes/auth/login/+page.svelte index 44e84ec..7c7277f 100644 --- a/frontend/src/routes/auth/login/+page.svelte +++ b/frontend/src/routes/auth/login/+page.svelte @@ -76,10 +76,6 @@ /> -
- Forgot password? -
-