google auth integration
This commit is contained in:
parent
9ee1d7f714
commit
68472b09b1
|
|
@ -11,7 +11,7 @@ INSERT INTO users (
|
||||||
phone_number,
|
phone_number,
|
||||||
role,
|
role,
|
||||||
password,
|
password,
|
||||||
age,
|
age_group,
|
||||||
education_level,
|
education_level,
|
||||||
country,
|
country,
|
||||||
region,
|
region,
|
||||||
|
|
@ -44,7 +44,7 @@ VALUES
|
||||||
NULL,
|
NULL,
|
||||||
'USER',
|
'USER',
|
||||||
crypt('password@123', gen_salt('bf'))::bytea,
|
crypt('password@123', gen_salt('bf'))::bytea,
|
||||||
22,
|
'25_34',
|
||||||
'Bachelor',
|
'Bachelor',
|
||||||
'Ethiopia',
|
'Ethiopia',
|
||||||
'Addis Ababa',
|
'Addis Ababa',
|
||||||
|
|
@ -76,7 +76,7 @@ VALUES
|
||||||
'0911001100',
|
'0911001100',
|
||||||
'ADMIN',
|
'ADMIN',
|
||||||
crypt('password@123', gen_salt('bf'))::bytea,
|
crypt('password@123', gen_salt('bf'))::bytea,
|
||||||
28,
|
'35_44',
|
||||||
'Master',
|
'Master',
|
||||||
'Ethiopia',
|
'Ethiopia',
|
||||||
'Addis Ababa',
|
'Addis Ababa',
|
||||||
|
|
@ -108,7 +108,7 @@ VALUES
|
||||||
'0911223344',
|
'0911223344',
|
||||||
'SUPPORT',
|
'SUPPORT',
|
||||||
crypt('password@123', gen_salt('bf'))::bytea,
|
crypt('password@123', gen_salt('bf'))::bytea,
|
||||||
25,
|
'55_PLUS',
|
||||||
'Diploma',
|
'Diploma',
|
||||||
'Ethiopia',
|
'Ethiopia',
|
||||||
'Addis Ababa',
|
'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 (
|
CREATE TABLE IF NOT EXISTS assessment_questions (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
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
|
-- name: IsUserPending :one
|
||||||
SELECT
|
SELECT
|
||||||
CASE WHEN status = 'PENDING' THEN true ELSE false END AS is_pending
|
CASE WHEN status = 'PENDING' THEN true ELSE false END AS is_pending
|
||||||
|
|
@ -120,6 +146,11 @@ SELECT *
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetUserByGoogleID :one
|
||||||
|
SELECT *
|
||||||
|
FROM users
|
||||||
|
WHERE google_id = $1;
|
||||||
|
|
||||||
|
|
||||||
-- name: GetAllUsers :many
|
-- name: GetAllUsers :many
|
||||||
SELECT
|
SELECT
|
||||||
|
|
|
||||||
|
|
@ -242,4 +242,6 @@ type User struct {
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
AgeGroup pgtype.Text `json:"age_group"`
|
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
|
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
|
const CreateUser = `-- name: CreateUser :one
|
||||||
INSERT INTO users (
|
INSERT INTO users (
|
||||||
first_name,
|
first_name,
|
||||||
|
|
@ -545,8 +624,54 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
|
||||||
return i, err
|
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
|
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
|
FROM users
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
@ -585,6 +710,8 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.AgeGroup,
|
&i.AgeGroup,
|
||||||
|
&i.GoogleID,
|
||||||
|
&i.GoogleEmailVerified,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -633,6 +760,25 @@ func (q *Queries) IsUserPending(ctx context.Context, id int64) (bool, error) {
|
||||||
return is_pending, err
|
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
|
const SearchUserByNameOrPhone = `-- name: SearchUserByNameOrPhone :many
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
|
|
||||||
6
go.mod
6
go.mod
|
|
@ -15,7 +15,11 @@ require (
|
||||||
golang.org/x/crypto v0.45.0
|
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 (
|
require (
|
||||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
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/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 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
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.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 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
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-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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/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"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type GoogleUser struct {
|
||||||
|
ID string
|
||||||
|
Email string
|
||||||
|
VerifiedEmail bool
|
||||||
|
GivenName string
|
||||||
|
FamilyName string
|
||||||
|
Picture string
|
||||||
|
}
|
||||||
|
|
||||||
type LoginSuccess struct {
|
type LoginSuccess struct {
|
||||||
UserId int64
|
UserId int64
|
||||||
Role Role
|
Role Role
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserStore interface {
|
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)
|
IsProfileCompleted(ctx context.Context, userId int64) (bool, error)
|
||||||
UpdateUserStatus(ctx context.Context, req domain.UpdateUserStatusReq) error
|
UpdateUserStatus(ctx context.Context, req domain.UpdateUserStatusReq) error
|
||||||
// GetCorrectOptionForQuestion(
|
// GetCorrectOptionForQuestion(
|
||||||
|
|
@ -34,6 +36,10 @@ type UserStore interface {
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
user domain.User,
|
user domain.User,
|
||||||
) (domain.User, error)
|
) (domain.User, error)
|
||||||
|
GetUserByGoogleID(
|
||||||
|
ctx context.Context,
|
||||||
|
googleId string,
|
||||||
|
) (domain.User, error)
|
||||||
GetUserByID(
|
GetUserByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id int64,
|
id int64,
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,43 @@ import (
|
||||||
|
|
||||||
func NewUserStore(s *Store) ports.UserStore { return s }
|
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) {
|
func (s *Store) IsProfileCompleted(ctx context.Context, userId int64) (bool, error) {
|
||||||
IsProfileCompleted, err := s.queries.IsProfileCompleted(ctx, userId)
|
IsProfileCompleted, err := s.queries.IsProfileCompleted(ctx, userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -280,6 +317,66 @@ func (s *Store) GetUserByID(
|
||||||
}, nil
|
}, 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
|
// GetAllUsers retrieves users with optional filters
|
||||||
func (s *Store) GetAllUsers(
|
func (s *Store) GetAllUsers(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
|
@ -736,3 +833,46 @@ func mapCreateUserResult(
|
||||||
UpdatedAt: updatedAt,
|
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"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -31,6 +32,71 @@ type loginUserRes struct {
|
||||||
UserID int64 `json:"user_id"`
|
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
|
// Loginuser godoc
|
||||||
// @Summary Login user
|
// @Summary Login user
|
||||||
// @Description Login user
|
// @Description Login user
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,8 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Post("/levels", h.CreateLevel)
|
groupV1.Post("/levels", h.CreateLevel)
|
||||||
|
|
||||||
// Auth Routes
|
// 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/customer-login", h.LoginUser)
|
||||||
groupV1.Post("/auth/admin-login", h.LoginAdmin)
|
groupV1.Post("/auth/admin-login", h.LoginAdmin)
|
||||||
groupV1.Post("/auth/super-login", h.LoginSuper)
|
groupV1.Post("/auth/super-login", h.LoginSuper)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user