174 lines
5.1 KiB
Go
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,
|
|
},
|
|
})
|
|
}
|