Files
noteplace-server/internal/api/auth.go
T
haopengzhan 55d4eb2e26
Go CI / build (push) Successful in 46s
feat: added web based auth path
2025-09-20 00:29:41 -07:00

142 lines
4.9 KiB
Go

package api
import (
"net/http"
"time"
"git.pengzhan.dev/noteplace-server/internal/auth"
"git.pengzhan.dev/noteplace-server/internal/models"
"git.pengzhan.dev/noteplace-server/internal/store"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
"golang.org/x/oauth2"
)
// AuthHandler handles the authentication API endpoints.
type AuthHandler struct {
Store *store.Store
Authenticator *auth.Authenticator
FrontendURL string
}
// HandleGoogleLogin redirects the user to the Google consent page for the web flow.
func (h *AuthHandler) HandleGoogleLogin(c *gin.Context) {
url := h.Authenticator.GetWebAuthURL()
zap.S().Infof("Redirecting user to Google for web authentication: %s", url)
c.Redirect(http.StatusTemporaryRedirect, url)
}
// HandleGoogleCallback handles the redirect from Google for the web flow.
func (h *AuthHandler) HandleGoogleCallback(c *gin.Context) {
code := c.Query("code")
if code == "" {
zap.S().Warn("Web auth callback received without a code.")
c.JSON(http.StatusBadRequest, gin.H{"error": "code is missing"})
return
}
zap.S().Info("Received web auth code, exchanging for token.")
token, err := h.Authenticator.ExchangeWebCodeForToken(code)
if err != nil {
zap.S().Errorf("Failed to exchange web code for token: %v", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Failed to validate code"})
return
}
h.handleToken(c, token)
}
// HandleCliLogin provides the user with the URL to start the auth process for the CLI flow.
func (h *AuthHandler) HandleCliLogin(c *gin.Context) {
authURL := h.Authenticator.GetCliAuthURL()
zap.S().Info("Providing CLI user with auth URL.")
response := gin.H{
"message": `Please open the following URL in your browser to authorize the application.\nAfter authorizing, Google will provide a code. Paste it here to complete the login.\nExample: curl -X POST -d '{\"code\":\"YOUR_CODE_HERE\"}' http://localhost:3001/api/auth/google/cli/callback`,
"url": authURL,
}
c.JSON(http.StatusOK, response)
}
// HandleCliCallback exchanges the code for a token and creates a session for the CLI flow.
func (h *AuthHandler) HandleCliCallback(c *gin.Context) {
var reqBody struct {
Code string `json:"code"`
}
if err := c.ShouldBindJSON(&reqBody); err != nil {
zap.S().Warnf("Invalid request body for CLI callback: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
zap.S().Info("Received CLI auth code, exchanging for token.")
token, err := h.Authenticator.ExchangeCliCodeForToken(reqBody.Code)
if err != nil {
zap.S().Errorf("Failed to exchange cli code for token: %v", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Failed to validate code"})
return
}
h.handleToken(c, token)
}
// handleToken is a helper function to handle user/session creation after getting a token.
func (h *AuthHandler) handleToken(c *gin.Context, token *oauth2.Token) {
zap.S().Info("Token acquired, fetching user info.")
userInfo, err := h.Authenticator.GetUserInfo(token)
if err != nil {
zap.S().Errorf("Failed to get user info: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user info from Google"})
return
}
user, found := h.Store.GetUserByGoogleID(userInfo.GoogleID)
if !found {
zap.S().Infof("User with Google ID %s not found, creating new user.", userInfo.GoogleID)
userInfo.ID = "user-" + uuid.New().String()
userInfo.CreatedAt = time.Now().UTC().Format(time.RFC3339)
if err := h.Store.CreateUser(userInfo); err != nil {
zap.S().Errorf("Failed to create user: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
return
}
user = userInfo
} else {
zap.S().Infof("Found existing user %s for Google ID %s.", user.ID, user.GoogleID)
}
sessionID := "session-" + uuid.New().String()
now := time.Now().UTC().Format(time.RFC3339)
newSession := models.Session{
ID: sessionID,
UserID: user.ID,
CreatedAt: now,
UpdatedAt: now,
}
if err := h.Store.CreateSession(newSession); err != nil {
zap.S().Errorf("Failed to create session: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create session"})
return
}
zap.S().Infof("Created new session %s for user %s.", newSession.ID, user.ID)
// Redirect back to the frontend for the web flow
if c.Request.Method == "GET" {
// Simple way to distinguish web callback from CLI POST
redirectURL := h.FrontendURL + "/auth/callback?token=" + newSession.ID
zap.S().Infof("Redirecting to frontend: %s", redirectURL)
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
} else {
// Respond with JSON for the CLI flow
zap.S().Info("Returning session token to CLI client.")
c.JSON(http.StatusOK, gin.H{"message": "Login successful!", "session_token": newSession.ID})
}
}
// HandleGetUser returns the current user based on the session token.
func (h *AuthHandler) HandleGetUser(c *gin.Context) {
user := c.MustGet("user").(models.User)
c.JSON(http.StatusOK, user)
}