253 lines
6.6 KiB
Go
253 lines
6.6 KiB
Go
package handler
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
"xboard-go/internal/config"
|
|
"xboard-go/internal/database"
|
|
"xboard-go/internal/model"
|
|
"xboard-go/internal/service"
|
|
"xboard-go/pkg/utils"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type LoginRequest struct {
|
|
Email string `json:"email" binding:"required,email"`
|
|
Password string `json:"password" binding:"required"`
|
|
}
|
|
|
|
type RegisterRequest struct {
|
|
Email string `json:"email" binding:"required,email"`
|
|
Password string `json:"password" binding:"required,min=8"`
|
|
InviteCode *string `json:"invite_code"`
|
|
}
|
|
|
|
func Login(c *gin.Context) {
|
|
var req LoginRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
Fail(c, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
var user model.User
|
|
if err := database.DB.Where("email = ?", req.Email).First(&user).Error; err != nil {
|
|
Fail(c, http.StatusUnauthorized, "email or password is incorrect")
|
|
return
|
|
}
|
|
|
|
if !utils.CheckPassword(req.Password, user.Password, user.PasswordAlgo, user.PasswordSalt) {
|
|
Fail(c, http.StatusUnauthorized, "email or password is incorrect")
|
|
return
|
|
}
|
|
|
|
token, err := utils.GenerateToken(user.ID, user.IsAdmin)
|
|
if err != nil {
|
|
Fail(c, http.StatusInternalServerError, "failed to create auth token")
|
|
return
|
|
}
|
|
|
|
now := time.Now().Unix()
|
|
_ = database.DB.Model(&model.User{}).Where("id = ?", user.ID).Update("last_login_at", now).Error
|
|
service.TrackSession(user.ID, token, c.ClientIP(), c.GetHeader("User-Agent"))
|
|
|
|
Success(c, gin.H{
|
|
"token": token,
|
|
"auth_data": token,
|
|
"is_admin": user.IsAdmin,
|
|
})
|
|
}
|
|
|
|
func Register(c *gin.Context) {
|
|
var req RegisterRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
Fail(c, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
var count int64
|
|
database.DB.Model(&model.User{}).Where("email = ?", req.Email).Count(&count)
|
|
if count > 0 {
|
|
Fail(c, http.StatusBadRequest, "email already exists")
|
|
return
|
|
}
|
|
|
|
hashedPassword, err := utils.HashPassword(req.Password)
|
|
if err != nil {
|
|
Fail(c, http.StatusInternalServerError, "failed to hash password")
|
|
return
|
|
}
|
|
|
|
now := time.Now().Unix()
|
|
tokenRaw := fmt.Sprintf("%x", md5.Sum([]byte(time.Now().String()+req.Email)))
|
|
user := model.User{
|
|
Email: req.Email,
|
|
Password: hashedPassword,
|
|
UUID: uuid.New().String(),
|
|
Token: tokenRaw[:16],
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
if err := database.DB.Create(&user).Error; err != nil {
|
|
Fail(c, http.StatusInternalServerError, "register failed")
|
|
return
|
|
}
|
|
|
|
token, err := utils.GenerateToken(user.ID, user.IsAdmin)
|
|
if err != nil {
|
|
Fail(c, http.StatusInternalServerError, "failed to create auth token")
|
|
return
|
|
}
|
|
service.TrackSession(user.ID, token, c.ClientIP(), c.GetHeader("User-Agent"))
|
|
|
|
Success(c, gin.H{
|
|
"token": token,
|
|
"auth_data": token,
|
|
"is_admin": user.IsAdmin,
|
|
})
|
|
}
|
|
|
|
func Token2Login(c *gin.Context) {
|
|
verify := strings.TrimSpace(c.Query("verify"))
|
|
if verify == "" {
|
|
Fail(c, http.StatusBadRequest, "verify token is required")
|
|
return
|
|
}
|
|
|
|
userID, ok := service.ResolveQuickLoginToken(verify)
|
|
if !ok {
|
|
Fail(c, http.StatusUnauthorized, "verify token is invalid or expired")
|
|
return
|
|
}
|
|
|
|
var user model.User
|
|
if err := database.DB.First(&user, userID).Error; err != nil {
|
|
Fail(c, http.StatusNotFound, "user not found")
|
|
return
|
|
}
|
|
|
|
token, err := utils.GenerateToken(user.ID, user.IsAdmin)
|
|
if err != nil {
|
|
Fail(c, http.StatusInternalServerError, "failed to create auth token")
|
|
return
|
|
}
|
|
service.TrackSession(user.ID, token, c.ClientIP(), c.GetHeader("User-Agent"))
|
|
_ = database.DB.Model(&model.User{}).Where("id = ?", user.ID).Update("last_login_at", time.Now().Unix()).Error
|
|
|
|
Success(c, gin.H{
|
|
"token": token,
|
|
"auth_data": token,
|
|
"is_admin": user.IsAdmin,
|
|
})
|
|
}
|
|
|
|
func SendEmailVerify(c *gin.Context) {
|
|
var req struct {
|
|
Email string `json:"email" binding:"required,email"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
Fail(c, http.StatusBadRequest, "invalid email")
|
|
return
|
|
}
|
|
|
|
code, err := randomDigits(6)
|
|
if err != nil {
|
|
Fail(c, http.StatusInternalServerError, "failed to generate verify code")
|
|
return
|
|
}
|
|
|
|
email := strings.ToLower(strings.TrimSpace(req.Email))
|
|
subject := fmt.Sprintf("[%s] Email verification code", service.MustGetString("app_name", "XBoard"))
|
|
textBody := strings.Join([]string{
|
|
"Your email verification code is:",
|
|
code,
|
|
"",
|
|
"This code will expire in 10 minutes.",
|
|
}, "\n")
|
|
htmlBody := fmt.Sprintf(
|
|
"<h2>Email verification</h2><p>Your verification code is <strong style=\"font-size:24px;letter-spacing:4px;\">%s</strong>.</p><p>This code will expire in 10 minutes.</p>",
|
|
code,
|
|
)
|
|
if err := service.SendMailWithCurrentSettings(service.EmailMessage{
|
|
To: []string{email},
|
|
Subject: subject,
|
|
TextBody: textBody,
|
|
HTMLBody: htmlBody,
|
|
}); err != nil {
|
|
Fail(c, http.StatusInternalServerError, "failed to send verification email: "+err.Error())
|
|
return
|
|
}
|
|
|
|
service.StoreEmailVerifyCode(email, code, 10*time.Minute)
|
|
data := gin.H{
|
|
"email": req.Email,
|
|
"expires_in": 600,
|
|
}
|
|
if config.AppConfig.AppDebug {
|
|
data["debug_code"] = code
|
|
}
|
|
SuccessMessage(c, "email verify code generated", data)
|
|
}
|
|
|
|
func ForgetPassword(c *gin.Context) {
|
|
var req struct {
|
|
Email string `json:"email" binding:"required,email"`
|
|
EmailCode string `json:"email_code" binding:"required"`
|
|
Password string `json:"password" binding:"required,min=8"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
Fail(c, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
email := strings.ToLower(strings.TrimSpace(req.Email))
|
|
if !service.CheckEmailVerifyCode(email, strings.TrimSpace(req.EmailCode)) {
|
|
Fail(c, http.StatusBadRequest, "email verify code is invalid")
|
|
return
|
|
}
|
|
|
|
hashed, err := utils.HashPassword(req.Password)
|
|
if err != nil {
|
|
Fail(c, http.StatusInternalServerError, "failed to hash password")
|
|
return
|
|
}
|
|
|
|
updates := map[string]any{
|
|
"password": hashed,
|
|
"password_algo": nil,
|
|
"password_salt": nil,
|
|
"updated_at": time.Now().Unix(),
|
|
}
|
|
if err := database.DB.Model(&model.User{}).Where("email = ?", email).Updates(updates).Error; err != nil {
|
|
Fail(c, http.StatusInternalServerError, "password reset failed")
|
|
return
|
|
}
|
|
|
|
SuccessMessage(c, "password reset success", true)
|
|
}
|
|
|
|
func randomDigits(length int) (string, error) {
|
|
if length <= 0 {
|
|
return "", nil
|
|
}
|
|
|
|
buf := make([]byte, length)
|
|
if _, err := rand.Read(buf); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
encoded := hex.EncodeToString(buf)
|
|
digits := make([]byte, 0, length)
|
|
for i := 0; i < len(encoded) && len(digits) < length; i++ {
|
|
digits = append(digits, '0'+(encoded[i]%10))
|
|
}
|
|
return string(digits), nil
|
|
}
|