+62
-10
@@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -10,17 +9,48 @@ import (
|
||||
"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
|
||||
}
|
||||
|
||||
// HandleCliLogin provides the user with the URL to start the auth process.
|
||||
// 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.GetAuthURL()
|
||||
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`,
|
||||
@@ -29,43 +59,53 @@ func (h *AuthHandler) HandleCliLogin(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// HandleCliCallback exchanges the code for a token and creates a session.
|
||||
// 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
|
||||
}
|
||||
|
||||
token, err := h.Authenticator.ExchangeCodeForToken(reqBody.Code)
|
||||
zap.S().Info("Received CLI auth code, exchanging for token.")
|
||||
token, err := h.Authenticator.ExchangeCliCodeForToken(reqBody.Code)
|
||||
if err != nil {
|
||||
log.Printf("Failed to exchange code for token: %v", err)
|
||||
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 {
|
||||
log.Printf("Failed to get user info: %v", err)
|
||||
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 and Session Logic ---
|
||||
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)
|
||||
}
|
||||
|
||||
// Create a new session and save it to the store
|
||||
sessionID := "session-" + uuid.New().String()
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
newSession := models.Session{
|
||||
@@ -75,11 +115,23 @@ func (h *AuthHandler) HandleCliCallback(c *gin.Context) {
|
||||
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)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Login successful!", "session_token": newSession.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.
|
||||
|
||||
+42
-17
@@ -14,44 +14,69 @@ import (
|
||||
|
||||
// Authenticator handles the OAuth2 flow.
|
||||
type Authenticator struct {
|
||||
Config *oauth2.Config
|
||||
WebConfig *oauth2.Config
|
||||
CliConfig *oauth2.Config
|
||||
}
|
||||
|
||||
// NewAuthenticator creates a new Authenticator.
|
||||
// It requires GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables.
|
||||
func NewAuthenticator() (*Authenticator, error) {
|
||||
clientID := os.Getenv("GOOGLE_CLIENT_ID")
|
||||
clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
|
||||
cliClientID := os.Getenv("CLI_GOOGLE_CLIENT_ID")
|
||||
cliclientSecret := os.Getenv("CLI_GOOGLE_CLIENT_SECRET")
|
||||
webClientID := os.Getenv("WEB_GOOGLE_CLIENT_ID")
|
||||
webClientSecret := os.Getenv("WEB_GOOGLE_CLIENT_SECRET")
|
||||
|
||||
if clientID == "" || clientSecret == "" {
|
||||
return nil, fmt.Errorf("GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must be set")
|
||||
if cliClientID == "" || cliclientSecret == "" {
|
||||
return nil, fmt.Errorf("CLI_GOOGLE_CLIENT_ID and CLI_GOOGLE_CLIENT_SECRET must be set for CLI")
|
||||
}
|
||||
|
||||
config := &oauth2.Config{
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
if webClientID == "" || webClientSecret == "" {
|
||||
return nil, fmt.Errorf("WEB_GOOGLE_CLIENT_ID and WEB_GOOGLE_CLIENT_SECRET must be set for web")
|
||||
}
|
||||
|
||||
webConfig := &oauth2.Config{
|
||||
ClientID: webClientID,
|
||||
ClientSecret: webClientSecret,
|
||||
RedirectURL: "http://localhost:3001/api/auth/google/callback", // For web flow
|
||||
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"},
|
||||
Endpoint: google.Endpoint,
|
||||
}
|
||||
|
||||
cliConfig := &oauth2.Config{
|
||||
ClientID: cliClientID,
|
||||
ClientSecret: cliclientSecret,
|
||||
RedirectURL: "urn:ietf:wg:oauth:2.0:oob", // Special redirect URI for desktop/CLI apps
|
||||
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"},
|
||||
Endpoint: google.Endpoint,
|
||||
}
|
||||
|
||||
return &Authenticator{Config: config}, nil
|
||||
return &Authenticator{WebConfig: webConfig, CliConfig: cliConfig}, nil
|
||||
}
|
||||
|
||||
// GetAuthURL generates the URL the user must visit to authorize the application.
|
||||
func (a *Authenticator) GetAuthURL() string {
|
||||
// We don't need a state for this simple CLI flow, but it's good practice for web apps.
|
||||
return a.Config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
|
||||
// GetWebAuthURL generates the URL for the web-based auth flow.
|
||||
func (a *Authenticator) GetWebAuthURL() string {
|
||||
return a.WebConfig.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
|
||||
}
|
||||
|
||||
// ExchangeCodeForToken takes an authorization code and exchanges it for a token.
|
||||
func (a *Authenticator) ExchangeCodeForToken(code string) (*oauth2.Token, error) {
|
||||
return a.Config.Exchange(context.Background(), code)
|
||||
// GetCliAuthURL generates the URL for the CLI-based auth flow.
|
||||
func (a *Authenticator) GetCliAuthURL() string {
|
||||
return a.CliConfig.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
|
||||
}
|
||||
|
||||
// ExchangeWebCodeForToken takes an authorization code from the web flow and exchanges it for a token.
|
||||
func (a *Authenticator) ExchangeWebCodeForToken(code string) (*oauth2.Token, error) {
|
||||
return a.WebConfig.Exchange(context.Background(), code)
|
||||
}
|
||||
|
||||
// ExchangeCliCodeForToken takes an authorization code from the CLI flow and exchanges it for a token.
|
||||
func (a *Authenticator) ExchangeCliCodeForToken(code string) (*oauth2.Token, error) {
|
||||
return a.CliConfig.Exchange(context.Background(), code)
|
||||
}
|
||||
|
||||
// GetUserInfo uses the token to fetch the user's profile from Google.
|
||||
func (a *Authenticator) GetUserInfo(token *oauth2.Token) (models.User, error) {
|
||||
client := a.Config.Client(context.Background(), token)
|
||||
// This client can be created from either WebConfig or CliConfig, it doesn't matter which.
|
||||
client := a.WebConfig.Client(context.Background(), token)
|
||||
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
|
||||
if err != nil {
|
||||
return models.User{}, err
|
||||
|
||||
Reference in New Issue
Block a user