Files
SproutGate/sproutgate-backend/internal/handlers/auth_login.go
2026-03-20 20:42:33 +08:00

174 lines
5.1 KiB
Go

package handlers
import (
"errors"
"net/http"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
"sproutgate-backend/internal/auth"
"sproutgate-backend/internal/clientgeo"
"sproutgate-backend/internal/models"
)
func (h *Handler) Login(c *gin.Context) {
var req loginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
req.Account = strings.TrimSpace(req.Account)
if req.Account == "" || req.Password == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "account and password are required"})
return
}
user, found, err := h.store.GetUser(req.Account)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load user"})
return
}
if !found {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
return
}
if user.Banned {
writeBanJSON(c, user.BanReason)
return
}
token, expiresAt, err := auth.GenerateToken(h.store.JWTSecret(), h.store.JWTIssuer(), user.Account, 7*24*time.Hour)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"})
return
}
if cid, ok := models.NormalizeAuthClientID(req.ClientID); ok {
name := models.ClampAuthClientName(req.ClientName)
if rec, err := h.store.RecordAuthClient(req.Account, cid, name); err == nil {
user = rec
}
}
c.JSON(http.StatusOK, gin.H{
"token": token,
"expiresAt": expiresAt.Format(time.RFC3339),
"user": user.OwnerPublic(),
})
}
func (h *Handler) Verify(c *gin.Context) {
var req verifyRequest
if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.Token) == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "token is required"})
return
}
claims, err := auth.ParseToken(h.store.JWTSecret(), h.store.JWTIssuer(), req.Token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"valid": false, "error": "invalid token"})
return
}
user, found, err := h.store.GetUser(claims.Account)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load user"})
return
}
if !found {
c.JSON(http.StatusUnauthorized, gin.H{"valid": false, "error": "user not found"})
return
}
if user.Banned {
h := gin.H{"valid": false, "error": "account is banned"}
if r := strings.TrimSpace(user.BanReason); r != "" {
h["banReason"] = r
}
c.JSON(http.StatusOK, h)
return
}
if cid, cname, ok := authClientFromHeaders(c); ok {
_, _ = h.store.RecordAuthClient(claims.Account, cid, cname)
}
c.JSON(http.StatusOK, gin.H{"valid": true, "user": user.Public()})
}
func (h *Handler) Me(c *gin.Context) {
token := bearerToken(c.GetHeader("Authorization"))
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing token"})
return
}
claims, err := auth.ParseToken(h.store.JWTSecret(), h.store.JWTIssuer(), token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
}
user, found, err := h.store.GetUser(claims.Account)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load user"})
return
}
if !found {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"})
return
}
if abortIfUserBanned(c, user) {
return
}
if cid, cname, ok := authClientFromHeaders(c); ok {
if rec, err := h.store.RecordAuthClient(claims.Account, cid, cname); err == nil {
user = rec
}
}
today := models.CurrentActivityDate()
nowAt := models.CurrentActivityTime()
user, _, err = h.store.RecordVisit(claims.Account, today, nowAt)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save visit"})
return
}
visitIP := strings.TrimSpace(c.GetHeader("X-Visit-Ip"))
visitLoc := strings.TrimSpace(c.GetHeader("X-Visit-Location"))
if visitIP == "" {
visitIP = strings.TrimSpace(c.ClientIP())
}
lookupURL := strings.TrimSpace(os.Getenv("GEO_LOOKUP_URL"))
if lookupURL == "" {
lookupURL = clientgeo.DefaultLookupURL
}
if visitLoc == "" && visitIP != "" {
if loc, geoErr := clientgeo.FetchDisplayLocation(c.Request.Context(), lookupURL, visitIP); geoErr == nil {
visitLoc = loc
}
}
if visitIP != "" || visitLoc != "" {
user, err = h.store.UpdateLastVisitMeta(claims.Account, visitIP, visitLoc)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save visit meta"})
return
}
}
checkInConfig := h.store.CheckInConfig()
c.JSON(http.StatusOK, gin.H{
"user": user.OwnerPublic(),
"checkIn": gin.H{
"rewardCoins": checkInConfig.RewardCoins,
"checkedInToday": user.LastCheckInDate == today,
"lastCheckInDate": user.LastCheckInDate,
"lastCheckInAt": user.LastCheckInAt,
"today": today,
},
})
}