From 68472b09b1bc3f5951a81a6245db84d973b7d242 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Wed, 21 Jan 2026 12:43:02 -0800 Subject: [PATCH] google auth integration --- db/data/001_initial_seed_data.sql | 8 +- db/migrations/000001_yimaru.up.sql | 11 ++ db/query/user.sql | 31 ++++ gen/db/models.go | 2 + gen/db/user.sql.go | 148 ++++++++++++++++++- go.mod | 6 +- go.sum | 4 + internal/domain/auth.go | 9 ++ internal/ports/user.go | 6 + internal/repository/user.go | 140 ++++++++++++++++++ internal/services/authentication/google.go | 137 +++++++++++++++++ internal/web_server/handlers/auth_handler.go | 66 +++++++++ internal/web_server/routes.go | 2 + 13 files changed, 564 insertions(+), 6 deletions(-) create mode 100644 internal/services/authentication/google.go diff --git a/db/data/001_initial_seed_data.sql b/db/data/001_initial_seed_data.sql index 2729418..9904e48 100644 --- a/db/data/001_initial_seed_data.sql +++ b/db/data/001_initial_seed_data.sql @@ -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', diff --git a/db/migrations/000001_yimaru.up.sql b/db/migrations/000001_yimaru.up.sql index 415f050..b0a25f2 100644 --- a/db/migrations/000001_yimaru.up.sql +++ b/db/migrations/000001_yimaru.up.sql @@ -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, diff --git a/db/query/user.sql b/db/query/user.sql index c78e332..60bf10a 100644 --- a/db/query/user.sql +++ b/db/query/user.sql @@ -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 diff --git a/gen/db/models.go b/gen/db/models.go index 78cbddc..1fc1f9a 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -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"` } diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index 8ff099e..83e1a69 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -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, diff --git a/go.mod b/go.mod index 135888f..7f120b4 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 6f7e85b..8531567 100644 --- a/go.sum +++ b/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= diff --git a/internal/domain/auth.go b/internal/domain/auth.go index e0c668b..bbead22 100644 --- a/internal/domain/auth.go +++ b/internal/domain/auth.go @@ -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 diff --git a/internal/ports/user.go b/internal/ports/user.go index a0cd54f..3024a8a 100644 --- a/internal/ports/user.go +++ b/internal/ports/user.go @@ -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, diff --git a/internal/repository/user.go b/internal/repository/user.go index bce49ff..dff0e22 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -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, + } +} diff --git a/internal/services/authentication/google.go b/internal/services/authentication/google.go new file mode 100644 index 0000000..4915d4e --- /dev/null +++ b/internal/services/authentication/google.go @@ -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 +} diff --git a/internal/web_server/handlers/auth_handler.go b/internal/web_server/handlers/auth_handler.go index 5035a10..91719c8 100644 --- a/internal/web_server/handlers/auth_handler.go +++ b/internal/web_server/handlers/auth_handler.go @@ -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 diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index fbe1592..181c6f4 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -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)