google auth integration
This commit is contained in:
parent
9ee1d7f714
commit
68472b09b1
|
|
@ -11,7 +11,7 @@ INSERT INTO users (
|
|||
phone_number,
|
||||
role,
|
||||
password,
|
||||
age,
|
||||
age_group,
|
||||
education_level,
|
||||
country,
|
||||
region,
|
||||
|
|
@ -44,7 +44,7 @@ VALUES
|
|||
NULL,
|
||||
'USER',
|
||||
crypt('password@123', gen_salt('bf'))::bytea,
|
||||
22,
|
||||
'25_34',
|
||||
'Bachelor',
|
||||
'Ethiopia',
|
||||
'Addis Ababa',
|
||||
|
|
@ -76,7 +76,7 @@ VALUES
|
|||
'0911001100',
|
||||
'ADMIN',
|
||||
crypt('password@123', gen_salt('bf'))::bytea,
|
||||
28,
|
||||
'35_44',
|
||||
'Master',
|
||||
'Ethiopia',
|
||||
'Addis Ababa',
|
||||
|
|
@ -108,7 +108,7 @@ VALUES
|
|||
'0911223344',
|
||||
'SUPPORT',
|
||||
crypt('password@123', gen_salt('bf'))::bytea,
|
||||
25,
|
||||
'55_PLUS',
|
||||
'Diploma',
|
||||
'Ethiopia',
|
||||
'Addis Ababa',
|
||||
|
|
|
|||
|
|
@ -63,6 +63,17 @@
|
|||
)
|
||||
);
|
||||
|
||||
ALTER TABLE users
|
||||
ADD COLUMN google_id VARCHAR(255) UNIQUE,
|
||||
ADD COLUMN google_email_verified BOOLEAN DEFAULT FALSE;
|
||||
|
||||
ALTER TABLE users
|
||||
ALTER COLUMN password DROP NOT NULL;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_google_id
|
||||
ON users (google_id)
|
||||
WHERE google_id IS NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS assessment_questions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,29 @@
|
|||
-- name: CreateGoogleUser :one
|
||||
INSERT INTO users (
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
google_id,
|
||||
google_email_verified,
|
||||
role,
|
||||
status,
|
||||
email_verified,
|
||||
profile_picture_url,
|
||||
profile_completed
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, true, $8, false
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: LinkGoogleAccount :exec
|
||||
UPDATE users
|
||||
SET
|
||||
google_id = $2,
|
||||
google_email_verified = true,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: IsUserPending :one
|
||||
SELECT
|
||||
CASE WHEN status = 'PENDING' THEN true ELSE false END AS is_pending
|
||||
|
|
@ -120,6 +146,11 @@ SELECT *
|
|||
FROM users
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: GetUserByGoogleID :one
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE google_id = $1;
|
||||
|
||||
|
||||
-- name: GetAllUsers :many
|
||||
SELECT
|
||||
|
|
|
|||
|
|
@ -242,4 +242,6 @@ type User struct {
|
|||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
AgeGroup pgtype.Text `json:"age_group"`
|
||||
GoogleID pgtype.Text `json:"google_id"`
|
||||
GoogleEmailVerified pgtype.Bool `json:"google_email_verified"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,85 @@ func (q *Queries) CheckPhoneEmailExist(ctx context.Context, arg CheckPhoneEmailE
|
|||
return i, err
|
||||
}
|
||||
|
||||
const CreateGoogleUser = `-- name: CreateGoogleUser :one
|
||||
INSERT INTO users (
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
google_id,
|
||||
google_email_verified,
|
||||
role,
|
||||
status,
|
||||
email_verified,
|
||||
profile_picture_url,
|
||||
profile_completed
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, true, $8, false
|
||||
)
|
||||
RETURNING id, first_name, last_name, gender, birth_day, email, phone_number, role, password, education_level, country, region, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favourite_topic, initial_assessment_completed, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at, age_group, google_id, google_email_verified
|
||||
`
|
||||
|
||||
type CreateGoogleUserParams struct {
|
||||
FirstName pgtype.Text `json:"first_name"`
|
||||
LastName pgtype.Text `json:"last_name"`
|
||||
Email pgtype.Text `json:"email"`
|
||||
GoogleID pgtype.Text `json:"google_id"`
|
||||
GoogleEmailVerified pgtype.Bool `json:"google_email_verified"`
|
||||
Role string `json:"role"`
|
||||
Status string `json:"status"`
|
||||
ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateGoogleUser(ctx context.Context, arg CreateGoogleUserParams) (User, error) {
|
||||
row := q.db.QueryRow(ctx, CreateGoogleUser,
|
||||
arg.FirstName,
|
||||
arg.LastName,
|
||||
arg.Email,
|
||||
arg.GoogleID,
|
||||
arg.GoogleEmailVerified,
|
||||
arg.Role,
|
||||
arg.Status,
|
||||
arg.ProfilePictureUrl,
|
||||
)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.Gender,
|
||||
&i.BirthDay,
|
||||
&i.Email,
|
||||
&i.PhoneNumber,
|
||||
&i.Role,
|
||||
&i.Password,
|
||||
&i.EducationLevel,
|
||||
&i.Country,
|
||||
&i.Region,
|
||||
&i.KnowledgeLevel,
|
||||
&i.NickName,
|
||||
&i.Occupation,
|
||||
&i.LearningGoal,
|
||||
&i.LanguageGoal,
|
||||
&i.LanguageChallange,
|
||||
&i.FavouriteTopic,
|
||||
&i.InitialAssessmentCompleted,
|
||||
&i.EmailVerified,
|
||||
&i.PhoneVerified,
|
||||
&i.Status,
|
||||
&i.LastLogin,
|
||||
&i.ProfileCompleted,
|
||||
&i.ProfilePictureUrl,
|
||||
&i.PreferredLanguage,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.AgeGroup,
|
||||
&i.GoogleID,
|
||||
&i.GoogleEmailVerified,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const CreateUser = `-- name: CreateUser :one
|
||||
INSERT INTO users (
|
||||
first_name,
|
||||
|
|
@ -545,8 +624,54 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
|
|||
return i, err
|
||||
}
|
||||
|
||||
const GetUserByGoogleID = `-- name: GetUserByGoogleID :one
|
||||
SELECT id, first_name, last_name, gender, birth_day, email, phone_number, role, password, education_level, country, region, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favourite_topic, initial_assessment_completed, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at, age_group, google_id, google_email_verified
|
||||
FROM users
|
||||
WHERE google_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByGoogleID(ctx context.Context, googleID pgtype.Text) (User, error) {
|
||||
row := q.db.QueryRow(ctx, GetUserByGoogleID, googleID)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.Gender,
|
||||
&i.BirthDay,
|
||||
&i.Email,
|
||||
&i.PhoneNumber,
|
||||
&i.Role,
|
||||
&i.Password,
|
||||
&i.EducationLevel,
|
||||
&i.Country,
|
||||
&i.Region,
|
||||
&i.KnowledgeLevel,
|
||||
&i.NickName,
|
||||
&i.Occupation,
|
||||
&i.LearningGoal,
|
||||
&i.LanguageGoal,
|
||||
&i.LanguageChallange,
|
||||
&i.FavouriteTopic,
|
||||
&i.InitialAssessmentCompleted,
|
||||
&i.EmailVerified,
|
||||
&i.PhoneVerified,
|
||||
&i.Status,
|
||||
&i.LastLogin,
|
||||
&i.ProfileCompleted,
|
||||
&i.ProfilePictureUrl,
|
||||
&i.PreferredLanguage,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.AgeGroup,
|
||||
&i.GoogleID,
|
||||
&i.GoogleEmailVerified,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetUserByID = `-- name: GetUserByID :one
|
||||
SELECT id, first_name, last_name, gender, birth_day, email, phone_number, role, password, education_level, country, region, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favourite_topic, initial_assessment_completed, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at, age_group
|
||||
SELECT id, first_name, last_name, gender, birth_day, email, phone_number, role, password, education_level, country, region, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favourite_topic, initial_assessment_completed, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at, age_group, google_id, google_email_verified
|
||||
FROM users
|
||||
WHERE id = $1
|
||||
`
|
||||
|
|
@ -585,6 +710,8 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.AgeGroup,
|
||||
&i.GoogleID,
|
||||
&i.GoogleEmailVerified,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -633,6 +760,25 @@ func (q *Queries) IsUserPending(ctx context.Context, id int64) (bool, error) {
|
|||
return is_pending, err
|
||||
}
|
||||
|
||||
const LinkGoogleAccount = `-- name: LinkGoogleAccount :exec
|
||||
UPDATE users
|
||||
SET
|
||||
google_id = $2,
|
||||
google_email_verified = true,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
type LinkGoogleAccountParams struct {
|
||||
ID int64 `json:"id"`
|
||||
GoogleID pgtype.Text `json:"google_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) LinkGoogleAccount(ctx context.Context, arg LinkGoogleAccountParams) error {
|
||||
_, err := q.db.Exec(ctx, LinkGoogleAccount, arg.ID, arg.GoogleID)
|
||||
return err
|
||||
}
|
||||
|
||||
const SearchUserByNameOrPhone = `-- name: SearchUserByNameOrPhone :many
|
||||
SELECT
|
||||
id,
|
||||
|
|
|
|||
6
go.mod
6
go.mod
|
|
@ -15,7 +15,11 @@ require (
|
|||
golang.org/x/crypto v0.45.0
|
||||
)
|
||||
|
||||
require github.com/rogpeppe/go-internal v1.8.1 // indirect
|
||||
require (
|
||||
cloud.google.com/go/compute/metadata v0.3.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.8.1 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -1,3 +1,5 @@
|
|||
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
|
|
@ -201,6 +203,8 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su
|
|||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
|
|||
|
|
@ -4,6 +4,15 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
type GoogleUser struct {
|
||||
ID string
|
||||
Email string
|
||||
VerifiedEmail bool
|
||||
GivenName string
|
||||
FamilyName string
|
||||
Picture string
|
||||
}
|
||||
|
||||
type LoginSuccess struct {
|
||||
UserId int64
|
||||
Role Role
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import (
|
|||
)
|
||||
|
||||
type UserStore interface {
|
||||
CreateGoogleUser(ctx context.Context, gUser domain.GoogleUser) (domain.User, error)
|
||||
LinkGoogleAccount(ctx context.Context, userID int64, googleID string) error
|
||||
IsProfileCompleted(ctx context.Context, userId int64) (bool, error)
|
||||
UpdateUserStatus(ctx context.Context, req domain.UpdateUserStatusReq) error
|
||||
// GetCorrectOptionForQuestion(
|
||||
|
|
@ -34,6 +36,10 @@ type UserStore interface {
|
|||
ctx context.Context,
|
||||
user domain.User,
|
||||
) (domain.User, error)
|
||||
GetUserByGoogleID(
|
||||
ctx context.Context,
|
||||
googleId string,
|
||||
) (domain.User, error)
|
||||
GetUserByID(
|
||||
ctx context.Context,
|
||||
id int64,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,43 @@ import (
|
|||
|
||||
func NewUserStore(s *Store) ports.UserStore { return s }
|
||||
|
||||
func (s *Store) LinkGoogleAccount(
|
||||
ctx context.Context,
|
||||
userID int64,
|
||||
googleID string,
|
||||
) error {
|
||||
|
||||
return s.queries.LinkGoogleAccount(ctx, dbgen.LinkGoogleAccountParams{
|
||||
ID: userID,
|
||||
GoogleID: pgtype.Text{String: googleID, Valid: true},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) CreateGoogleUser(
|
||||
ctx context.Context,
|
||||
gUser domain.GoogleUser,
|
||||
) (domain.User, error) {
|
||||
|
||||
res, err := s.queries.CreateGoogleUser(ctx, dbgen.CreateGoogleUserParams{
|
||||
FirstName: pgtype.Text{String: gUser.GivenName, Valid: true},
|
||||
LastName: pgtype.Text{String: gUser.FamilyName, Valid: true},
|
||||
Email: pgtype.Text{String: gUser.Email, Valid: true},
|
||||
GoogleID: pgtype.Text{String: gUser.ID, Valid: true},
|
||||
GoogleEmailVerified: pgtype.Bool{Bool: gUser.VerifiedEmail, Valid: true},
|
||||
Role: string(domain.RoleStudent),
|
||||
Status: string(domain.UserStatusActive),
|
||||
ProfilePictureUrl: pgtype.Text{
|
||||
String: gUser.Picture,
|
||||
Valid: gUser.Picture != "",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return domain.User{}, err
|
||||
}
|
||||
|
||||
return mapDBUser(res, nil, nil), nil
|
||||
}
|
||||
|
||||
func (s *Store) IsProfileCompleted(ctx context.Context, userId int64) (bool, error) {
|
||||
IsProfileCompleted, err := s.queries.IsProfileCompleted(ctx, userId)
|
||||
if err != nil {
|
||||
|
|
@ -280,6 +317,66 @@ func (s *Store) GetUserByID(
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetUserByGoogleID(
|
||||
ctx context.Context,
|
||||
googleId string,
|
||||
) (domain.User, error) {
|
||||
|
||||
u, err := s.queries.GetUserByGoogleID(ctx, pgtype.Text{String: googleId, Valid: googleId != ""})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.User{}, domain.ErrUserNotFound
|
||||
}
|
||||
return domain.User{}, err
|
||||
}
|
||||
|
||||
var lastLogin *time.Time
|
||||
if u.LastLogin.Valid {
|
||||
lastLogin = &u.LastLogin.Time
|
||||
}
|
||||
|
||||
var updatedAt *time.Time
|
||||
if u.UpdatedAt.Valid {
|
||||
updatedAt = &u.UpdatedAt.Time
|
||||
}
|
||||
|
||||
return domain.User{
|
||||
ID: u.ID,
|
||||
FirstName: u.FirstName.String,
|
||||
LastName: u.LastName.String,
|
||||
Gender: u.Gender.String,
|
||||
BirthDay: u.BirthDay.Time,
|
||||
// UserName: u.UserName,
|
||||
Email: u.Email.String,
|
||||
PhoneNumber: u.PhoneNumber.String,
|
||||
Role: domain.Role(u.Role),
|
||||
|
||||
AgeGroup: u.AgeGroup.String,
|
||||
EducationLevel: u.EducationLevel.String,
|
||||
Country: u.Country.String,
|
||||
Region: u.Region.String,
|
||||
|
||||
NickName: u.NickName.String,
|
||||
Occupation: u.Occupation.String,
|
||||
LearningGoal: u.LearningGoal.String,
|
||||
LanguageGoal: u.LanguageGoal.String,
|
||||
LanguageChallange: u.LanguageChallange.String,
|
||||
FavouriteTopic: u.FavouriteTopic.String,
|
||||
|
||||
EmailVerified: u.EmailVerified,
|
||||
PhoneVerified: u.PhoneVerified,
|
||||
Status: domain.UserStatus(u.Status),
|
||||
|
||||
LastLogin: lastLogin,
|
||||
ProfileCompleted: u.ProfileCompleted.Bool,
|
||||
ProfilePictureURL: u.ProfilePictureUrl.String,
|
||||
PreferredLanguage: u.PreferredLanguage.String,
|
||||
|
||||
CreatedAt: u.CreatedAt.Time,
|
||||
UpdatedAt: updatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAllUsers retrieves users with optional filters
|
||||
func (s *Store) GetAllUsers(
|
||||
ctx context.Context,
|
||||
|
|
@ -736,3 +833,46 @@ func mapCreateUserResult(
|
|||
UpdatedAt: updatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// mapDBUser converts dbgen.User to domain.User (used by CreateGoogleUser)
|
||||
func mapDBUser(
|
||||
userRes dbgen.User,
|
||||
password []byte,
|
||||
updatedAt *time.Time,
|
||||
) domain.User {
|
||||
|
||||
return domain.User{
|
||||
ID: userRes.ID,
|
||||
FirstName: userRes.FirstName.String,
|
||||
LastName: userRes.LastName.String,
|
||||
Gender: userRes.Gender.String,
|
||||
BirthDay: userRes.BirthDay.Time,
|
||||
// UserName: userRes.UserName,
|
||||
Email: userRes.Email.String,
|
||||
PhoneNumber: userRes.PhoneNumber.String,
|
||||
Role: domain.Role(userRes.Role),
|
||||
Password: password,
|
||||
|
||||
AgeGroup: userRes.AgeGroup.String,
|
||||
EducationLevel: userRes.EducationLevel.String,
|
||||
Country: userRes.Country.String,
|
||||
Region: userRes.Region.String,
|
||||
|
||||
NickName: userRes.NickName.String,
|
||||
Occupation: userRes.Occupation.String,
|
||||
LearningGoal: userRes.LearningGoal.String,
|
||||
LanguageGoal: userRes.LanguageGoal.String,
|
||||
LanguageChallange: userRes.LanguageChallange.String,
|
||||
FavouriteTopic: userRes.FavouriteTopic.String,
|
||||
|
||||
EmailVerified: userRes.EmailVerified,
|
||||
PhoneVerified: userRes.PhoneVerified,
|
||||
Status: domain.UserStatus(userRes.Status),
|
||||
|
||||
ProfileCompleted: userRes.ProfileCompleted.Bool,
|
||||
PreferredLanguage: userRes.PreferredLanguage.String,
|
||||
|
||||
CreatedAt: userRes.CreatedAt.Time,
|
||||
UpdatedAt: updatedAt,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
137
internal/services/authentication/google.go
Normal file
137
internal/services/authentication/google.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
)
|
||||
|
||||
var googleOAuthConfig *oauth2.Config
|
||||
|
||||
func (s *Service) InitGoogleOAuth(clientID, clientSecret, redirectURL string) {
|
||||
googleOAuthConfig = &oauth2.Config{
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
RedirectURL: redirectURL,
|
||||
Scopes: []string{
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
"https://www.googleapis.com/auth/userinfo.profile",
|
||||
},
|
||||
Endpoint: google.Endpoint,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) GenerateGoogleLoginURL(state string) string {
|
||||
return googleOAuthConfig.AuthCodeURL(state, oauth2.AccessTypeOffline)
|
||||
}
|
||||
|
||||
func (s *Service) ExchangeGoogleCode(
|
||||
ctx context.Context,
|
||||
code string,
|
||||
) (*oauth2.Token, error) {
|
||||
if code == "" {
|
||||
return nil, errors.New("missing google auth code")
|
||||
}
|
||||
return googleOAuthConfig.Exchange(ctx, code)
|
||||
}
|
||||
|
||||
func (s *Service) FetchGoogleUser(
|
||||
ctx context.Context,
|
||||
token *oauth2.Token,
|
||||
) (*domain.GoogleUser, error) {
|
||||
|
||||
client := googleOAuthConfig.Client(ctx, token)
|
||||
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch google user: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var user domain.GoogleUser
|
||||
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode google user: %w", err)
|
||||
}
|
||||
|
||||
if user.Email == "" {
|
||||
return nil, errors.New("google account has no email")
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (s *Service) LoginWithGoogle(
|
||||
ctx context.Context,
|
||||
gUser domain.GoogleUser,
|
||||
) (domain.LoginSuccess, error) {
|
||||
|
||||
// 1. Try Google ID login
|
||||
user, err := s.userStore.GetUserByGoogleID(ctx, gUser.ID)
|
||||
if err != nil {
|
||||
|
||||
// 2. Try account linking by email
|
||||
user, err = s.userStore.GetUserByEmailPhone(ctx, gUser.Email, "")
|
||||
if err != nil {
|
||||
|
||||
// 3. Create new user
|
||||
user, err = s.userStore.CreateGoogleUser(ctx, gUser)
|
||||
if err != nil {
|
||||
return domain.LoginSuccess{}, err
|
||||
}
|
||||
|
||||
} else {
|
||||
// Link Google account
|
||||
if err := s.userStore.LinkGoogleAccount(ctx, user.ID, gUser.ID); err != nil {
|
||||
return domain.LoginSuccess{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Status checks (identical to Login)
|
||||
if user.Status == domain.UserStatusPending {
|
||||
return domain.LoginSuccess{}, domain.ErrUserNotVerified
|
||||
}
|
||||
|
||||
if user.Status == domain.UserStatusSuspended {
|
||||
return domain.LoginSuccess{}, ErrUserSuspended
|
||||
}
|
||||
|
||||
// 5. Revoke existing refresh token
|
||||
oldToken, err := s.tokenStore.GetRefreshTokenByUserID(ctx, user.ID)
|
||||
if err != nil && !errors.Is(err, ErrRefreshTokenNotFound) {
|
||||
return domain.LoginSuccess{}, err
|
||||
}
|
||||
|
||||
if err == nil && !oldToken.Revoked {
|
||||
if err := s.tokenStore.RevokeRefreshToken(ctx, oldToken.Token); err != nil {
|
||||
return domain.LoginSuccess{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Generate new refresh token
|
||||
refreshToken, err := generateRefreshToken()
|
||||
if err != nil {
|
||||
return domain.LoginSuccess{}, err
|
||||
}
|
||||
|
||||
if err := s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{
|
||||
Token: refreshToken,
|
||||
UserID: user.ID,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second),
|
||||
}); err != nil {
|
||||
return domain.LoginSuccess{}, err
|
||||
}
|
||||
|
||||
// 7. Return standard response
|
||||
return domain.LoginSuccess{
|
||||
UserId: user.ID,
|
||||
Role: user.Role,
|
||||
RfToken: refreshToken,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
|
|
@ -31,6 +32,71 @@ type loginUserRes struct {
|
|||
UserID int64 `json:"user_id"`
|
||||
}
|
||||
|
||||
// GoogleLogin godoc
|
||||
// @Summary Google login redirect
|
||||
// @Tags auth
|
||||
// @Router /api/v1/auth/google/login [get]
|
||||
func (h *Handler) GoogleLogin(c *fiber.Ctx) error {
|
||||
state := uuid.NewString()
|
||||
return c.Redirect(h.authSvc.GenerateGoogleLoginURL(state))
|
||||
}
|
||||
|
||||
// GoogleCallback godoc
|
||||
// @Summary Google login callback
|
||||
// @Tags auth
|
||||
// @Router /api/v1/auth/google/callback [get]
|
||||
func (h *Handler) GoogleCallback(c *fiber.Ctx) error {
|
||||
|
||||
code := c.Query("code")
|
||||
|
||||
token, err := h.authSvc.ExchangeGoogleCode(c.Context(), code)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Google authentication failed",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
gUser, err := h.authSvc.FetchGoogleUser(c.Context(), token)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to fetch Google user",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
loginRes, err := h.authSvc.LoginWithGoogle(c.Context(), *gUser)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
|
||||
Message: "Login failed",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
accessToken, err := jwtutil.CreateJwt(
|
||||
loginRes.UserId,
|
||||
loginRes.Role,
|
||||
h.jwtConfig.JwtAccessKey,
|
||||
h.jwtConfig.JwtAccessExpiry,
|
||||
)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Token generation failed",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||
Message: "Login successful",
|
||||
Data: fiber.Map{
|
||||
"accessToken": accessToken,
|
||||
"refreshToken": loginRes.RfToken,
|
||||
"userId": loginRes.UserId,
|
||||
"role": loginRes.Role,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Loginuser godoc
|
||||
// @Summary Login user
|
||||
// @Description Login user
|
||||
|
|
|
|||
|
|
@ -152,6 +152,8 @@ func (a *App) initAppRoutes() {
|
|||
groupV1.Post("/levels", h.CreateLevel)
|
||||
|
||||
// Auth Routes
|
||||
groupV1.Get("/auth/google/login", h.GoogleLogin)
|
||||
groupV1.Get("/auth/google/callback", h.GoogleCallback)
|
||||
groupV1.Post("/auth/customer-login", h.LoginUser)
|
||||
groupV1.Post("/auth/admin-login", h.LoginAdmin)
|
||||
groupV1.Post("/auth/super-login", h.LoginSuper)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user