Potion/internal/app/service/auth_service.go
Hayden Hargreaves b6a434ad2a (FEAT): Authentication service with Google OAuth is working!
Still missing a UI and we do not have session management just yet, but
the workflow of calling the Google API's and creating/finding users is
working with the current structure.
2025-06-14 12:35:08 -07:00

95 lines
3.5 KiB
Go

package service
import (
"context"
"fmt"
domainAuth "github.com/haydenhargreaves/Potion/internal/domain/auth"
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
"github.com/haydenhargreaves/Potion/internal/infrastructure/auth"
"golang.org/x/oauth2"
)
// NOTE: HOW THIS WORKS
//
// I need to store the refresh token along side the user in the DB.
// Create a "session token" (cookie) with an expiration and store that in the DB and the session.
// Send this session token along with all the requests (stored in the session) and when
// authorization is needed, use it.
// Once the expiration of MY token expires, prompt the user to log back in.
//
// So what is the point of the Google refresh token? Well, its not really useful right now, but if
// we need to perform Google actions, we can use it to get more access tokens, which are needed for
// the Google actions. Currently, I have no need for it, but it will be stored anyway.
//
// ------------------------------------------------------------------------------------------------
// AuthService implements the domain.AuthService defined in the domain module.
type AuthService struct {
userRepository domain.UserRepository
}
// Compile-time check to ensure the AuthService implements domain.AuthService
var _ domainAuth.AuthService = (*AuthService)(nil)
// NewAuthService creates a user service object which can be passed into the context. The service
// requires a user repository which it will use to hit the database when needed.
func NewAuthService(userRepository domain.UserRepository) domainAuth.AuthService {
return &AuthService{userRepository: userRepository}
}
// GetGoogleAuthUrl generates a URL which is used to redirect the user to the Google sign in page.
// This URL is in the Google domain and is not coupled with this application. The data from this URL
// will be passed into a callback and work to complete the Google OAuth workflow.
func (s *AuthService) GetGoogleAuthUrl() string {
url := auth.GoogleAuthConfig.AuthCodeURL(
"randomstate",
oauth2.AccessTypeOffline,
oauth2.ApprovalForce,
)
return url
}
// GoogleAuthSuccess accepts the data from the Google login endpoint and uses it to fetch the users
// data. The data is then used to log the user in or create an account.
func (s *AuthService) GoogleAuthSuccess(state, code string) (domain.User, domain.GoogleUserInfo, error) {
// Ensure the state matches, prevents M.I.T.M. attacks
if state != "randomstate" {
return domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("States don't match, received %s", state)
}
// Get access token from Google
token, err := auth.GoogleAuthConfig.Exchange(context.Background(), code)
if err != nil {
return domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Code exchange failed: %s", err.Error())
}
// Use the access token to get user data
googleUserInfo, err := auth.GetUserData(token.AccessToken)
if err != nil {
return domain.User{}, domain.GoogleUserInfo{}, err
}
// Attempt to get the user, user is nil when they don't exit
user, err := s.userRepository.GetGoogleUser(googleUserInfo.Id)
if err != nil {
return domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Failed to get db user: %s", err)
}
// A user was found
if user != nil {
return *user, googleUserInfo, nil
}
// user did not exist, need to create one
newUser, err := s.userRepository.CreateGoogleUser(&googleUserInfo, token.RefreshToken)
if err != nil {
return domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Repository failed to create user: %s", err.Error())
}
// temporary
return newUser, googleUserInfo, nil
}