google auth integration

This commit is contained in:
Yared Yemane 2026-01-21 12:43:02 -08:00
parent 9ee1d7f714
commit 68472b09b1
13 changed files with 564 additions and 6 deletions

View File

@ -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',

View File

@ -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,

View File

@ -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

View File

@ -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"`
} }

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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

View File

@ -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,

View File

@ -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,
}
}

View 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
}

View File

@ -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

View File

@ -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)