238 lines
6.8 KiB
Go
238 lines
6.8 KiB
Go
package public
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/mattnite/forgejo-tickets/internal/auth"
|
|
"github.com/mattnite/forgejo-tickets/internal/models"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// validatePassword checks password complexity requirements.
|
|
func validatePassword(password string) string {
|
|
if len(password) < 8 {
|
|
return "Password must be at least 8 characters"
|
|
}
|
|
var hasUpper, hasLower, hasDigit bool
|
|
for _, r := range password {
|
|
switch {
|
|
case unicode.IsUpper(r):
|
|
hasUpper = true
|
|
case unicode.IsLower(r):
|
|
hasLower = true
|
|
case unicode.IsDigit(r):
|
|
hasDigit = true
|
|
}
|
|
}
|
|
if !hasUpper || !hasLower || !hasDigit {
|
|
return "Password must contain at least one uppercase letter, one lowercase letter, and one digit"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
type AuthHandler struct {
|
|
deps Dependencies
|
|
}
|
|
|
|
func (h *AuthHandler) loginData(extra map[string]interface{}) map[string]interface{} {
|
|
data := map[string]interface{}{
|
|
"GoogleEnabled": h.deps.Config.GoogleClientID != "",
|
|
"MicrosoftEnabled": h.deps.Config.MicrosoftClientID != "",
|
|
"AppleEnabled": h.deps.Config.AppleClientID != "",
|
|
}
|
|
for k, v := range extra {
|
|
data[k] = v
|
|
}
|
|
return data
|
|
}
|
|
|
|
func (h *AuthHandler) LoginForm(c *gin.Context) {
|
|
if auth.CurrentUser(c) != nil {
|
|
c.Redirect(http.StatusSeeOther, "/tickets")
|
|
return
|
|
}
|
|
h.deps.Renderer.Render(c.Writer, c.Request, "login", h.loginData(nil))
|
|
}
|
|
|
|
func (h *AuthHandler) Login(c *gin.Context) {
|
|
email := strings.TrimSpace(c.PostForm("email"))
|
|
password := c.PostForm("password")
|
|
|
|
user, err := h.deps.Auth.Login(c.Request.Context(), email, password)
|
|
if err != nil {
|
|
h.deps.Renderer.Render(c.Writer, c.Request, "login", h.loginData(map[string]interface{}{
|
|
"Error": err.Error(),
|
|
"Email": email,
|
|
}))
|
|
return
|
|
}
|
|
|
|
if err := h.deps.Auth.CreateSession(c.Request, c.Writer, user.ID); err != nil {
|
|
log.Error().Err(err).Msg("create session error")
|
|
h.deps.Renderer.Render(c.Writer, c.Request, "login", h.loginData(map[string]interface{}{
|
|
"Error": "An unexpected error occurred",
|
|
"Email": email,
|
|
}))
|
|
return
|
|
}
|
|
|
|
c.Redirect(http.StatusSeeOther, "/tickets")
|
|
}
|
|
|
|
func (h *AuthHandler) RegisterForm(c *gin.Context) {
|
|
if auth.CurrentUser(c) != nil {
|
|
c.Redirect(http.StatusSeeOther, "/tickets")
|
|
return
|
|
}
|
|
h.deps.Renderer.Render(c.Writer, c.Request, "register", nil)
|
|
}
|
|
|
|
func (h *AuthHandler) Register(c *gin.Context) {
|
|
name := strings.TrimSpace(c.PostForm("name"))
|
|
email := strings.TrimSpace(c.PostForm("email"))
|
|
password := c.PostForm("password")
|
|
confirmPassword := c.PostForm("confirm_password")
|
|
|
|
data := map[string]interface{}{
|
|
"Name": name,
|
|
"Email": email,
|
|
}
|
|
|
|
if password != confirmPassword {
|
|
data["Error"] = "Passwords do not match"
|
|
h.deps.Renderer.Render(c.Writer, c.Request, "register", data)
|
|
return
|
|
}
|
|
|
|
if errMsg := validatePassword(password); errMsg != "" {
|
|
data["Error"] = errMsg
|
|
h.deps.Renderer.Render(c.Writer, c.Request, "register", data)
|
|
return
|
|
}
|
|
|
|
user, err := h.deps.Auth.Register(c.Request.Context(), email, password, name)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "duplicate key") || strings.Contains(err.Error(), "unique") {
|
|
data["Error"] = "An account with this email already exists"
|
|
} else {
|
|
data["Error"] = "Registration failed. Please try again."
|
|
}
|
|
h.deps.Renderer.Render(c.Writer, c.Request, "register", data)
|
|
return
|
|
}
|
|
|
|
token, err := h.deps.Auth.GenerateVerificationToken(c.Request.Context(), user.ID)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("generate verification token error")
|
|
} else {
|
|
if err := h.deps.EmailClient.SendVerificationEmail(user.Email, user.Name, token); err != nil {
|
|
log.Error().Err(err).Msg("send verification email error")
|
|
}
|
|
}
|
|
|
|
redirectURL := "/login?" + url.Values{
|
|
"flash": {"Account requested! Please check your email to verify your address. After verification, an admin will review your request."},
|
|
"flash_type": {"success"},
|
|
}.Encode()
|
|
c.Redirect(http.StatusSeeOther, redirectURL)
|
|
}
|
|
|
|
func (h *AuthHandler) Logout(c *gin.Context) {
|
|
if err := h.deps.Auth.DestroySession(c.Request, c.Writer); err != nil {
|
|
log.Error().Err(err).Msg("destroy session error")
|
|
}
|
|
c.Redirect(http.StatusSeeOther, "/")
|
|
}
|
|
|
|
func (h *AuthHandler) VerifyEmail(c *gin.Context) {
|
|
token := c.Query("token")
|
|
if token == "" {
|
|
h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusBadRequest, "Missing verification token")
|
|
return
|
|
}
|
|
|
|
_, err := h.deps.Auth.VerifyEmailToken(c.Request.Context(), token)
|
|
if err != nil {
|
|
h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusBadRequest, "Invalid or expired verification token")
|
|
return
|
|
}
|
|
|
|
redirectURL := "/login?" + url.Values{
|
|
"flash": {"Email verified successfully. Your account is pending admin approval."},
|
|
"flash_type": {"success"},
|
|
}.Encode()
|
|
c.Redirect(http.StatusSeeOther, redirectURL)
|
|
}
|
|
|
|
func (h *AuthHandler) ForgotPasswordForm(c *gin.Context) {
|
|
h.deps.Renderer.Render(c.Writer, c.Request, "forgot-password", nil)
|
|
}
|
|
|
|
func (h *AuthHandler) ForgotPassword(c *gin.Context) {
|
|
email := strings.TrimSpace(c.PostForm("email"))
|
|
|
|
var user models.User
|
|
if err := h.deps.DB.Where("email = ?", email).First(&user).Error; err == nil {
|
|
token, err := h.deps.Auth.GeneratePasswordResetToken(c.Request.Context(), user.ID)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("generate reset token error")
|
|
} else {
|
|
if err := h.deps.EmailClient.SendPasswordResetEmail(user.Email, user.Name, token); err != nil {
|
|
log.Error().Err(err).Msg("send reset email error")
|
|
}
|
|
}
|
|
}
|
|
|
|
h.deps.Renderer.Render(c.Writer, c.Request, "forgot-password", map[string]interface{}{
|
|
"Success": "If an account exists with that email, we've sent a password reset link.",
|
|
})
|
|
}
|
|
|
|
func (h *AuthHandler) ResetPasswordForm(c *gin.Context) {
|
|
token := c.Query("token")
|
|
h.deps.Renderer.Render(c.Writer, c.Request, "reset-password", map[string]interface{}{
|
|
"Token": token,
|
|
})
|
|
}
|
|
|
|
func (h *AuthHandler) ResetPassword(c *gin.Context) {
|
|
token := c.PostForm("token")
|
|
password := c.PostForm("password")
|
|
confirmPassword := c.PostForm("confirm_password")
|
|
|
|
if password != confirmPassword {
|
|
h.deps.Renderer.Render(c.Writer, c.Request, "reset-password", map[string]interface{}{
|
|
"Token": token,
|
|
"Error": "Passwords do not match",
|
|
})
|
|
return
|
|
}
|
|
|
|
if errMsg := validatePassword(password); errMsg != "" {
|
|
h.deps.Renderer.Render(c.Writer, c.Request, "reset-password", map[string]interface{}{
|
|
"Token": token,
|
|
"Error": errMsg,
|
|
})
|
|
return
|
|
}
|
|
|
|
_, err := h.deps.Auth.RedeemPasswordResetToken(c.Request.Context(), token, password)
|
|
if err != nil {
|
|
h.deps.Renderer.Render(c.Writer, c.Request, "reset-password", map[string]interface{}{
|
|
"Token": token,
|
|
"Error": "Invalid or expired reset token",
|
|
})
|
|
return
|
|
}
|
|
|
|
redirectURL := "/login?" + url.Values{
|
|
"flash": {"Password reset successfully. You can now log in."},
|
|
"flash_type": {"success"},
|
|
}.Encode()
|
|
c.Redirect(http.StatusSeeOther, redirectURL)
|
|
}
|