Potion/internal/app/service/auth_service.go
Hayden Hargreaves 9ac7356668 (DOC/FEAT): Updated doc comments and completed the search redirection!
The search is nearly complete for the initial implementation. Just need
to figure out what to do with the text search provided, make any
required UI changes, and eventual implement pagination via a "load more"
button.
2025-07-09 22:21:49 -07:00

127 lines
4.7 KiB
Go

package service
import (
"context"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
domainAuth "github.com/haydenhargreaves/Potion/internal/domain/auth"
domainServer "github.com/haydenhargreaves/Potion/internal/domain/server"
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
jwtSecret []byte
}
// 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, jwtSecret []byte) domainAuth.AuthService {
return &AuthService{
userRepository: userRepository,
jwtSecret: jwtSecret,
}
}
// 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) (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 {
jwt, err := generateJwt(user.Id, user.Email, s.jwtSecret)
return jwt, *user, googleUserInfo, err
}
// 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())
}
jwt, err := generateJwt(newUser.Id, newUser.Email, s.jwtSecret)
return jwt, newUser, googleUserInfo, err
}
// generateJwt requires user data and returns a JSON web token which can be stored in the browsers
// cookies. This token is used to log a user into the application and allow access to protected
// routes.
func generateJwt(userId int, email string, jwtSecret []byte) (string, error) {
expiration := time.Now().Add(7 * 24 * time.Hour)
claims := &domainServer.JwtClaims{
UserId: userId,
Email: email,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiration),
IssuedAt: jwt.NewNumericDate(time.Now()),
Subject: fmt.Sprintf("%d", userId),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtSecret)
if err != nil {
return "", fmt.Errorf("Failed to sign jwt: %s", err.Error())
}
return tokenString, nil
}