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, }, }) }