ForFarm/backend/internal/api/oauth.go

123 lines
4.3 KiB
Go

package api
import (
"context"
"crypto/rand"
"database/sql"
"errors"
"net/http"
"github.com/danielgtaylor/huma/v2"
"github.com/forfarm/backend/internal/domain"
"github.com/forfarm/backend/internal/utilities"
"github.com/go-chi/chi/v5"
"golang.org/x/crypto/bcrypt"
)
func (a *api) registerOauthRoutes(_ chi.Router, apiInstance huma.API) {
tags := []string{"oauth"}
huma.Register(apiInstance, huma.Operation{
OperationID: "oauth_exchange",
Method: http.MethodPost,
Path: "/oauth/exchange",
Tags: tags,
}, a.exchangeHandler)
}
type ExchangeTokenInput struct {
Body struct {
AccessToken string `json:"accessToken" required:"true" example:"Google ID token"`
}
}
type ExchangeTokenOutput struct {
Body struct {
JWT string `json:"jwt" example:"Fresh JWT for frontend authentication"`
Email string `json:"email" example:"Email address of the user"`
}
}
func generateRandomPassword(length int) (string, error) {
const charset = "abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+[]{}<>?,./"
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
for i, b := range bytes {
bytes[i] = charset[b%byte(len(charset))]
}
return string(bytes), nil
}
func (a *api) exchangeHandler(ctx context.Context, input *ExchangeTokenInput) (*ExchangeTokenOutput, error) {
if input.Body.AccessToken == "" {
return nil, huma.Error400BadRequest("accessToken is required") // Match JSON tag
}
googleUserID, email, err := utilities.ExtractGoogleUserID(input.Body.AccessToken)
if err != nil {
a.logger.Warn("Invalid Google ID token received", "error", err)
return nil, huma.Error401Unauthorized("Invalid Google ID token", err)
}
if email == "" {
a.logger.Error("Google token verification succeeded but email is missing", "googleUserId", googleUserID)
return nil, huma.Error500InternalServerError("Failed to retrieve email from Google token")
}
user, err := a.userRepo.GetByEmail(ctx, email)
if errors.Is(err, domain.ErrNotFound) || errors.Is(err, sql.ErrNoRows) {
a.logger.Info("Creating new user from Google OAuth", "email", email, "googleUserId", googleUserID)
newPassword, passErr := generateRandomPassword(16) // Increase length
if passErr != nil {
a.logger.Error("Failed to generate random password for OAuth user", "error", passErr)
return nil, huma.Error500InternalServerError("User creation failed (password generation)")
}
hashedPassword, hashErr := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if hashErr != nil {
a.logger.Error("Failed to hash generated password for OAuth user", "error", hashErr)
return nil, huma.Error500InternalServerError("User creation failed (password hashing)")
}
newUser := &domain.User{
Email: email,
Password: string(hashedPassword), // Store hashed random password
IsActive: true, // Activate user immediately
// Username can be initially empty or derived from email if needed
}
if createErr := a.userRepo.CreateOrUpdate(ctx, newUser); createErr != nil {
a.logger.Error("Failed to save new OAuth user to database", "email", email, "error", createErr)
return nil, huma.Error500InternalServerError("Failed to create user account")
}
user = *newUser
} else if err != nil {
a.logger.Error("Database error looking up user by email during OAuth", "email", email, "error", err)
return nil, huma.Error500InternalServerError("Failed to process login")
}
// Ensure the existing user is active
if !user.IsActive {
a.logger.Warn("OAuth login attempt for inactive user", "email", email, "user_uuid", user.UUID)
return nil, huma.Error403Forbidden("Account is inactive")
}
// Generate JWT for the user (either existing or newly created)
token, err := utilities.CreateJwtToken(user.UUID)
if err != nil {
a.logger.Error("Failed to create JWT token after OAuth exchange", "user_uuid", user.UUID, "error", err)
return nil, huma.Error500InternalServerError("Failed to generate session token")
}
output := &ExchangeTokenOutput{}
output.Body.JWT = token
output.Body.Email = email // Return the email for frontend context
_ = googleUserID // Maybe log or store this association if needed later
a.logger.Info("OAuth exchange successful", "email", email, "user_uuid", user.UUID)
return output, nil
}