(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.
This commit is contained in:
parent
71e32f1fd7
commit
b6a434ad2a
@ -4,18 +4,27 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/haydenhargreaves/Potion/internal/app/service"
|
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GoogleLogin(ctx *gin.Context) {
|
func GoogleLogin(ctx *gin.Context) {
|
||||||
url := service.GoogleLogin(ctx)
|
deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||||
|
url := deps.AuthService.GetGoogleAuthUrl()
|
||||||
|
|
||||||
ctx.Redirect(http.StatusSeeOther, url)
|
ctx.Redirect(http.StatusSeeOther, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GoogleCallback(ctx *gin.Context) {
|
func GoogleCallback(ctx *gin.Context) {
|
||||||
if googleUserInfo, err := service.GoogleCallback(ctx); err != nil {
|
deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
||||||
ctx.JSON(http.StatusInternalServerError, err)
|
|
||||||
|
var (
|
||||||
|
state string = ctx.Query("state")
|
||||||
|
code string = ctx.Query("code")
|
||||||
|
)
|
||||||
|
|
||||||
|
if dbUser, googleUserInfo, err := deps.AuthService.GoogleAuthSuccess(state, code); err != nil {
|
||||||
|
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
} else {
|
} else {
|
||||||
ctx.JSON(http.StatusOK, googleUserInfo)
|
ctx.JSON(http.StatusOK, gin.H{"googleUserInfo": googleUserInfo, "dbUser": dbUser})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
domainAuth "github.com/haydenhargreaves/Potion/internal/domain/auth"
|
||||||
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
|
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
|
||||||
"github.com/haydenhargreaves/Potion/internal/infrastructure/auth"
|
"github.com/haydenhargreaves/Potion/internal/infrastructure/auth"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
@ -24,10 +24,24 @@ import (
|
|||||||
//
|
//
|
||||||
// ------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// GoogleLogin generates a URL which is used to redirect the user to the Google sign in page. This
|
// AuthService implements the domain.AuthService defined in the domain module.
|
||||||
// URL is in the Google domain and is not coupled with this application. The data from this URL will
|
type AuthService struct {
|
||||||
// be passed into a callback and work to complete the Google OAuth workflow.
|
userRepository domain.UserRepository
|
||||||
func GoogleLogin(ctx *gin.Context) string {
|
}
|
||||||
|
|
||||||
|
// 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(
|
url := auth.GoogleAuthConfig.AuthCodeURL(
|
||||||
"randomstate",
|
"randomstate",
|
||||||
oauth2.AccessTypeOffline,
|
oauth2.AccessTypeOffline,
|
||||||
@ -37,34 +51,44 @@ func GoogleLogin(ctx *gin.Context) string {
|
|||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
// GoogleCallback accepts the data from the Google login endpoint and uses it to fetch the users
|
// 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.
|
// 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) {
|
||||||
// TODO: The repository implementation is not yet done.
|
|
||||||
func GoogleCallback(ctx *gin.Context) (domain.GoogleUserInfo, error) {
|
|
||||||
var (
|
|
||||||
state string = ctx.Query("state")
|
|
||||||
code string = ctx.Query("code")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure the state matches, prevents M.I.T.M. attacks
|
// Ensure the state matches, prevents M.I.T.M. attacks
|
||||||
if state != "randomstate" {
|
if state != "randomstate" {
|
||||||
return domain.GoogleUserInfo{}, fmt.Errorf("States don't match, received %s", state)
|
return domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("States don't match, received %s", state)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get access token from Google
|
// Get access token from Google
|
||||||
token, err := auth.GoogleAuthConfig.Exchange(context.Background(), code)
|
token, err := auth.GoogleAuthConfig.Exchange(context.Background(), code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.GoogleUserInfo{}, fmt.Errorf("Code exchange failed: %s", err.Error())
|
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)
|
googleUserInfo, err := auth.GetUserData(token.AccessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.GoogleUserInfo{}, err
|
return domain.User{}, domain.GoogleUserInfo{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Need to hit the repository to store the required data in the user table
|
// 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
|
// temporary
|
||||||
return googleUserInfo, nil
|
return newUser, googleUserInfo, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
10
internal/domain/auth/service.go
Normal file
10
internal/domain/auth/service.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthService interface {
|
||||||
|
GetGoogleAuthUrl() string
|
||||||
|
GoogleAuthSuccess(state, code string) (domain.User, domain.GoogleUserInfo, error)
|
||||||
|
}
|
||||||
11
internal/domain/server/server.go
Normal file
11
internal/domain/server/server.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
domainAuth "github.com/haydenhargreaves/Potion/internal/domain/auth"
|
||||||
|
domainUser "github.com/haydenhargreaves/Potion/internal/domain/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InjectedDependencies struct {
|
||||||
|
UserService domainUser.UserService
|
||||||
|
AuthService domainAuth.AuthService
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
type UserRepository interface {
|
||||||
|
CreateGoogleUser(googleUserInfo *GoogleUserInfo, googleRefreshToken string) (User, error)
|
||||||
|
GetGoogleUser(googleId string) (*User, error)
|
||||||
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
package domain
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
// GoogleUserInfo is a data type which contains a mapping of the Google User Info API call.
|
// GoogleUserInfo is a data type which contains a mapping of the Google User Info API call.
|
||||||
type GoogleUserInfo struct {
|
type GoogleUserInfo struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
@ -10,3 +12,15 @@ type GoogleUserInfo struct {
|
|||||||
FamilyName string `json:"family_name"`
|
FamilyName string `json:"family_name"`
|
||||||
Picture string `json:"picture"`
|
Picture string `json:"picture"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User is the database model of a user. There is no need to map to a different model so
|
||||||
|
// this will remain in the domain.
|
||||||
|
type User struct {
|
||||||
|
Id int
|
||||||
|
GoogleId string
|
||||||
|
Name string
|
||||||
|
Email string
|
||||||
|
ImageUrl string
|
||||||
|
GoogleRefreshToken string
|
||||||
|
Created time.Time
|
||||||
|
}
|
||||||
|
|||||||
107
internal/infrastructure/database/repository/user_repository.go
Normal file
107
internal/infrastructure/database/repository/user_repository.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserRepository struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time check to ensure the UserRepository implements domain.UserRepository
|
||||||
|
var _ domain.UserRepository = (*UserRepository)(nil)
|
||||||
|
|
||||||
|
// NewUserRepository creates a user repository object which is used by the user service to access
|
||||||
|
// the database. Any user related database operations will take place in this repository.
|
||||||
|
func NewUserRepository(db *sql.DB) domain.UserRepository {
|
||||||
|
return &UserRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateGoogleUser creates a user entry in the users table. The refresh token is required along
|
||||||
|
// with the Google user info, in order to complete the database schema. Currently, Google login is
|
||||||
|
// the only support method of authentication, if this changes, this repository may need updates to
|
||||||
|
// match an updated table schema.
|
||||||
|
//
|
||||||
|
// This function will NOT check if the user already exists, if they do, it will return an error. For
|
||||||
|
// best results, pair this function with the GetGoogleUser which will return the user if it can find
|
||||||
|
// it.
|
||||||
|
func (r *UserRepository) CreateGoogleUser(googleUserInfo *domain.GoogleUserInfo, googleRefreshToken string) (domain.User, error) {
|
||||||
|
tx, err := r.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return domain.User{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var user domain.User
|
||||||
|
query := `INSERT INTO users
|
||||||
|
(GoogleId, Name, Email, ImageUrl, GoogleRefreshToken)
|
||||||
|
VALUES ($1, $2, $3, $4, $5) RETURNING *;`
|
||||||
|
|
||||||
|
if err := tx.QueryRow(
|
||||||
|
query,
|
||||||
|
googleUserInfo.Id,
|
||||||
|
googleUserInfo.Name,
|
||||||
|
googleUserInfo.Email,
|
||||||
|
googleUserInfo.Picture,
|
||||||
|
googleRefreshToken,
|
||||||
|
).Scan(
|
||||||
|
&user.Id,
|
||||||
|
&user.GoogleId,
|
||||||
|
&user.Name,
|
||||||
|
&user.Email,
|
||||||
|
&user.ImageUrl,
|
||||||
|
&user.GoogleRefreshToken,
|
||||||
|
&user.Created,
|
||||||
|
); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return domain.User{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return domain.User{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGoogleUser attempts to find a user in the database via its Google ID, not the database ID. This
|
||||||
|
// function is used when a user logs in with Google to prevent duplicate entries from being made. If
|
||||||
|
// no user is found, this function will return a null pointer but not an error.
|
||||||
|
func (r *UserRepository) GetGoogleUser(googleId string) (*domain.User, error) {
|
||||||
|
tx, err := r.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var user domain.User
|
||||||
|
query := `SELECT * FROM users WHERE GoogleId = $1`
|
||||||
|
|
||||||
|
if err := tx.QueryRow(query, googleId).Scan(
|
||||||
|
&user.Id,
|
||||||
|
&user.GoogleId,
|
||||||
|
&user.Name,
|
||||||
|
&user.Email,
|
||||||
|
&user.ImageUrl,
|
||||||
|
&user.GoogleRefreshToken,
|
||||||
|
&user.Created,
|
||||||
|
); err != nil {
|
||||||
|
// If no user was found, don't error, just return
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
tx.Rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user