package auth import ( "context" "encoding/json" "fmt" "log" "os" "git.pengzhan.dev/noteplace-server/internal/models" "golang.org/x/oauth2" "golang.org/x/oauth2/google" ) // Authenticator handles the OAuth2 flow. type Authenticator struct { 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) { 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 cliClientID == "" || cliclientSecret == "" { return nil, fmt.Errorf("CLI_GOOGLE_CLIENT_ID and CLI_GOOGLE_CLIENT_SECRET must be set for CLI") } 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{WebConfig: webConfig, CliConfig: cliConfig}, nil } // GetWebAuthURL generates the URL for the web-based auth flow. func (a *Authenticator) GetWebAuthURL() string { return a.WebConfig.AuthCodeURL("state-token", oauth2.AccessTypeOffline) } // 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) { // 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 } defer func() { _ = resp.Body.Close() }() var userInfo struct { ID string `json:"id"` Email string `json:"email"` Name string `json:"name"` } if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil { return models.User{}, err } log.Printf("Fetched user info from Google: ID=%s, Name=%s", userInfo.ID, userInfo.Name) // We create a new User model from the Google info. user := models.User{ GoogleID: userInfo.ID, DisplayName: userInfo.Name, Email: userInfo.Email, } return user, nil }