109 lines
3.5 KiB
Go
109 lines
3.5 KiB
Go
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
|
|
}
|