apple sign in
This commit is contained in:
parent
c00ab684c5
commit
632371c3d0
5
db/migrations/000076_apple_sign_in.down.sql
Normal file
5
db/migrations/000076_apple_sign_in.down.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
DROP INDEX IF EXISTS idx_users_apple_id;
|
||||||
|
|
||||||
|
ALTER TABLE users
|
||||||
|
DROP COLUMN IF EXISTS apple_id,
|
||||||
|
DROP COLUMN IF EXISTS apple_email_verified;
|
||||||
7
db/migrations/000076_apple_sign_in.up.sql
Normal file
7
db/migrations/000076_apple_sign_in.up.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN IF NOT EXISTS apple_id VARCHAR(255),
|
||||||
|
ADD COLUMN IF NOT EXISTS apple_email_verified BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_apple_id
|
||||||
|
ON users (apple_id)
|
||||||
|
WHERE apple_id IS NOT NULL;
|
||||||
|
|
@ -23,6 +23,30 @@ SET
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: CreateAppleUser :one
|
||||||
|
INSERT INTO users (
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
email,
|
||||||
|
apple_id,
|
||||||
|
apple_email_verified,
|
||||||
|
role,
|
||||||
|
status,
|
||||||
|
email_verified
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
$1, $2, $3, $4, $5, $6, $7, $8
|
||||||
|
)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: LinkAppleAccount :exec
|
||||||
|
UPDATE users
|
||||||
|
SET
|
||||||
|
apple_id = $2,
|
||||||
|
apple_email_verified = $3,
|
||||||
|
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
|
||||||
|
|
@ -156,6 +180,10 @@ SELECT *
|
||||||
FROM users
|
FROM users
|
||||||
WHERE google_id = $1;
|
WHERE google_id = $1;
|
||||||
|
|
||||||
|
-- name: GetUserByAppleID :one
|
||||||
|
SELECT *
|
||||||
|
FROM users
|
||||||
|
WHERE apple_id = $1;
|
||||||
|
|
||||||
-- name: GetAllUsers :many
|
-- name: GetAllUsers :many
|
||||||
SELECT
|
SELECT
|
||||||
|
|
|
||||||
|
|
@ -571,6 +571,8 @@ type User struct {
|
||||||
DeletionRequestedAt pgtype.Timestamptz `json:"deletion_requested_at"`
|
DeletionRequestedAt pgtype.Timestamptz `json:"deletion_requested_at"`
|
||||||
DeletionScheduledAt pgtype.Timestamptz `json:"deletion_scheduled_at"`
|
DeletionScheduledAt pgtype.Timestamptz `json:"deletion_scheduled_at"`
|
||||||
DeletionCancelledAt pgtype.Timestamptz `json:"deletion_cancelled_at"`
|
DeletionCancelledAt pgtype.Timestamptz `json:"deletion_cancelled_at"`
|
||||||
|
AppleID pgtype.Text `json:"apple_id"`
|
||||||
|
AppleEmailVerified pgtype.Bool `json:"apple_email_verified"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserAudioResponse struct {
|
type UserAudioResponse struct {
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,89 @@ func (q *Queries) CheckPhoneEmailExist(ctx context.Context, arg CheckPhoneEmailE
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CreateAppleUser = `-- name: CreateAppleUser :one
|
||||||
|
INSERT INTO users (
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
email,
|
||||||
|
apple_id,
|
||||||
|
apple_email_verified,
|
||||||
|
role,
|
||||||
|
status,
|
||||||
|
email_verified
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
$1, $2, $3, $4, $5, $6, $7, $8
|
||||||
|
)
|
||||||
|
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, profile_completion_percentage, deletion_requested_at, deletion_scheduled_at, deletion_cancelled_at, apple_id, apple_email_verified
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateAppleUserParams struct {
|
||||||
|
FirstName pgtype.Text `json:"first_name"`
|
||||||
|
LastName pgtype.Text `json:"last_name"`
|
||||||
|
Email pgtype.Text `json:"email"`
|
||||||
|
AppleID pgtype.Text `json:"apple_id"`
|
||||||
|
AppleEmailVerified pgtype.Bool `json:"apple_email_verified"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
EmailVerified bool `json:"email_verified"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateAppleUser(ctx context.Context, arg CreateAppleUserParams) (User, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CreateAppleUser,
|
||||||
|
arg.FirstName,
|
||||||
|
arg.LastName,
|
||||||
|
arg.Email,
|
||||||
|
arg.AppleID,
|
||||||
|
arg.AppleEmailVerified,
|
||||||
|
arg.Role,
|
||||||
|
arg.Status,
|
||||||
|
arg.EmailVerified,
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
&i.ProfileCompletionPercentage,
|
||||||
|
&i.DeletionRequestedAt,
|
||||||
|
&i.DeletionScheduledAt,
|
||||||
|
&i.DeletionCancelledAt,
|
||||||
|
&i.AppleID,
|
||||||
|
&i.AppleEmailVerified,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const CreateGoogleUser = `-- name: CreateGoogleUser :one
|
const CreateGoogleUser = `-- name: CreateGoogleUser :one
|
||||||
INSERT INTO users (
|
INSERT INTO users (
|
||||||
first_name,
|
first_name,
|
||||||
|
|
@ -101,7 +184,7 @@ INSERT INTO users (
|
||||||
VALUES (
|
VALUES (
|
||||||
$1, $2, $3, $4, $5, $6, $7, true, $8
|
$1, $2, $3, $4, $5, $6, $7, true, $8
|
||||||
)
|
)
|
||||||
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, profile_completion_percentage, deletion_requested_at, deletion_scheduled_at, deletion_cancelled_at
|
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, profile_completion_percentage, deletion_requested_at, deletion_scheduled_at, deletion_cancelled_at, apple_id, apple_email_verified
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateGoogleUserParams struct {
|
type CreateGoogleUserParams struct {
|
||||||
|
|
@ -164,6 +247,8 @@ func (q *Queries) CreateGoogleUser(ctx context.Context, arg CreateGoogleUserPara
|
||||||
&i.DeletionRequestedAt,
|
&i.DeletionRequestedAt,
|
||||||
&i.DeletionScheduledAt,
|
&i.DeletionScheduledAt,
|
||||||
&i.DeletionCancelledAt,
|
&i.DeletionCancelledAt,
|
||||||
|
&i.AppleID,
|
||||||
|
&i.AppleEmailVerified,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -621,6 +706,58 @@ func (q *Queries) GetTotalUsers(ctx context.Context, role string) (int64, error)
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetUserByAppleID = `-- name: GetUserByAppleID :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, profile_completion_percentage, deletion_requested_at, deletion_scheduled_at, deletion_cancelled_at, apple_id, apple_email_verified
|
||||||
|
FROM users
|
||||||
|
WHERE apple_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetUserByAppleID(ctx context.Context, appleID pgtype.Text) (User, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetUserByAppleID, appleID)
|
||||||
|
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,
|
||||||
|
&i.ProfileCompletionPercentage,
|
||||||
|
&i.DeletionRequestedAt,
|
||||||
|
&i.DeletionScheduledAt,
|
||||||
|
&i.DeletionCancelledAt,
|
||||||
|
&i.AppleID,
|
||||||
|
&i.AppleEmailVerified,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const GetUserByEmailPhone = `-- name: GetUserByEmailPhone :one
|
const GetUserByEmailPhone = `-- name: GetUserByEmailPhone :one
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -768,7 +905,7 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetUserByGoogleID = `-- name: GetUserByGoogleID :one
|
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, profile_completion_percentage, deletion_requested_at, deletion_scheduled_at, deletion_cancelled_at
|
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, profile_completion_percentage, deletion_requested_at, deletion_scheduled_at, deletion_cancelled_at, apple_id, apple_email_verified
|
||||||
FROM users
|
FROM users
|
||||||
WHERE google_id = $1
|
WHERE google_id = $1
|
||||||
`
|
`
|
||||||
|
|
@ -813,12 +950,14 @@ func (q *Queries) GetUserByGoogleID(ctx context.Context, googleID pgtype.Text) (
|
||||||
&i.DeletionRequestedAt,
|
&i.DeletionRequestedAt,
|
||||||
&i.DeletionScheduledAt,
|
&i.DeletionScheduledAt,
|
||||||
&i.DeletionCancelledAt,
|
&i.DeletionCancelledAt,
|
||||||
|
&i.AppleID,
|
||||||
|
&i.AppleEmailVerified,
|
||||||
)
|
)
|
||||||
return i, err
|
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, google_id, google_email_verified, profile_completion_percentage, deletion_requested_at, deletion_scheduled_at, deletion_cancelled_at
|
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, profile_completion_percentage, deletion_requested_at, deletion_scheduled_at, deletion_cancelled_at, apple_id, apple_email_verified
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
@ -863,6 +1002,8 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
||||||
&i.DeletionRequestedAt,
|
&i.DeletionRequestedAt,
|
||||||
&i.DeletionScheduledAt,
|
&i.DeletionScheduledAt,
|
||||||
&i.DeletionCancelledAt,
|
&i.DeletionCancelledAt,
|
||||||
|
&i.AppleID,
|
||||||
|
&i.AppleEmailVerified,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -930,6 +1071,26 @@ func (q *Queries) IsUserPending(ctx context.Context, id int64) (bool, error) {
|
||||||
return is_pending, err
|
return is_pending, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LinkAppleAccount = `-- name: LinkAppleAccount :exec
|
||||||
|
UPDATE users
|
||||||
|
SET
|
||||||
|
apple_id = $2,
|
||||||
|
apple_email_verified = $3,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type LinkAppleAccountParams struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
AppleID pgtype.Text `json:"apple_id"`
|
||||||
|
AppleEmailVerified pgtype.Bool `json:"apple_email_verified"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) LinkAppleAccount(ctx context.Context, arg LinkAppleAccountParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, LinkAppleAccount, arg.ID, arg.AppleID, arg.AppleEmailVerified)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const LinkGoogleAccount = `-- name: LinkGoogleAccount :exec
|
const LinkGoogleAccount = `-- name: LinkGoogleAccount :exec
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET
|
SET
|
||||||
|
|
|
||||||
4
go.mod
4
go.mod
|
|
@ -29,7 +29,7 @@ require (
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect
|
||||||
github.com/MicahParks/keyfunc v1.9.0 // indirect
|
github.com/MicahParks/keyfunc v1.9.0 // direct
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
|
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
|
@ -40,7 +40,7 @@ require (
|
||||||
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
github.com/golang-jwt/jwt/v4 v4.5.2 // direct
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,8 @@ type Config struct {
|
||||||
GoogleOAuthClientID string
|
GoogleOAuthClientID string
|
||||||
GoogleOAuthClientSecret string
|
GoogleOAuthClientSecret string
|
||||||
GoogleOAuthRedirectURL string
|
GoogleOAuthRedirectURL string
|
||||||
|
// AppleSignInClientIDs is a comma-separated list of allowed "aud" values (iOS bundle ID, Services ID, etc.).
|
||||||
|
AppleSignInClientIDs string
|
||||||
AFROSMSConfig AFROSMSConfig `mapstructure:"afro_sms_config"`
|
AFROSMSConfig AFROSMSConfig `mapstructure:"afro_sms_config"`
|
||||||
Vimeo VimeoConfig `mapstructure:"vimeo_config"`
|
Vimeo VimeoConfig `mapstructure:"vimeo_config"`
|
||||||
CloudConvert CloudConvertConfig `mapstructure:"cloudconvert_config"`
|
CloudConvert CloudConvertConfig `mapstructure:"cloudconvert_config"`
|
||||||
|
|
@ -174,6 +176,7 @@ func (c *Config) loadEnv() error {
|
||||||
c.GoogleOAuthClientID = os.Getenv("GOOGLE_OAUTH_CLIENT_ID")
|
c.GoogleOAuthClientID = os.Getenv("GOOGLE_OAUTH_CLIENT_ID")
|
||||||
c.GoogleOAuthClientSecret = os.Getenv("GOOGLE_OAUTH_CLIENT_SECRET")
|
c.GoogleOAuthClientSecret = os.Getenv("GOOGLE_OAUTH_CLIENT_SECRET")
|
||||||
c.GoogleOAuthRedirectURL = os.Getenv("GOOGLE_OAUTH_REDIRECT_URL")
|
c.GoogleOAuthRedirectURL = os.Getenv("GOOGLE_OAUTH_REDIRECT_URL")
|
||||||
|
c.AppleSignInClientIDs = os.Getenv("APPLE_SIGN_IN_CLIENT_IDS")
|
||||||
|
|
||||||
c.APP_VERSION = os.Getenv("APP_VERSION")
|
c.APP_VERSION = os.Getenv("APP_VERSION")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,15 @@ type GoogleUser struct {
|
||||||
Picture string
|
Picture string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AppleUser is populated from a validated Sign in with Apple identity token and optional client-supplied profile fields.
|
||||||
|
type AppleUser struct {
|
||||||
|
ID string
|
||||||
|
Email string
|
||||||
|
VerifiedEmail bool
|
||||||
|
GivenName string
|
||||||
|
FamilyName string
|
||||||
|
}
|
||||||
|
|
||||||
type LoginSuccess struct {
|
type LoginSuccess struct {
|
||||||
UserId int64
|
UserId int64
|
||||||
Role Role
|
Role Role
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ type ProfileCompletionStatus struct {
|
||||||
type UserStore interface {
|
type UserStore interface {
|
||||||
CreateGoogleUser(ctx context.Context, gUser domain.GoogleUser) (domain.User, error)
|
CreateGoogleUser(ctx context.Context, gUser domain.GoogleUser) (domain.User, error)
|
||||||
LinkGoogleAccount(ctx context.Context, userID int64, googleID string) error
|
LinkGoogleAccount(ctx context.Context, userID int64, googleID string) error
|
||||||
|
CreateAppleUser(ctx context.Context, aUser domain.AppleUser) (domain.User, error)
|
||||||
|
LinkAppleAccount(ctx context.Context, userID int64, appleID string, emailVerified bool) error
|
||||||
GetProfileCompletionStatus(ctx context.Context, userId int64) (ProfileCompletionStatus, error)
|
GetProfileCompletionStatus(ctx context.Context, userId int64) (ProfileCompletionStatus, error)
|
||||||
UpdateUserStatus(ctx context.Context, req domain.UpdateUserStatusReq) error
|
UpdateUserStatus(ctx context.Context, req domain.UpdateUserStatusReq) error
|
||||||
// GetCorrectOptionForQuestion(
|
// GetCorrectOptionForQuestion(
|
||||||
|
|
@ -45,6 +47,10 @@ type UserStore interface {
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
googleId string,
|
googleId string,
|
||||||
) (domain.User, error)
|
) (domain.User, error)
|
||||||
|
GetUserByAppleID(
|
||||||
|
ctx context.Context,
|
||||||
|
appleID string,
|
||||||
|
) (domain.User, error)
|
||||||
GetUserByID(
|
GetUserByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id int64,
|
id int64,
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,11 @@ func (s *Store) LmsUserHasLessonProgress(ctx context.Context, userID, lessonID i
|
||||||
return s.queries.UserHasLessonProgress(ctx, dbgen.UserHasLessonProgressParams{UserID: userID, LessonID: lessonID})
|
return s.queries.UserHasLessonProgress(ctx, dbgen.UserHasLessonProgressParams{UserID: userID, LessonID: lessonID})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LmsCountPublishedPracticesInLesson returns how many published practices are attached to a lesson.
|
||||||
|
func (s *Store) LmsCountPublishedPracticesInLesson(ctx context.Context, lessonID int64) (int32, error) {
|
||||||
|
return s.queries.CountPublishedPracticesInLesson(ctx, toPgInt8(&lessonID))
|
||||||
|
}
|
||||||
|
|
||||||
// LmsUserPracticeProgressInLesson returns published practice completion counts scoped to a lesson.
|
// LmsUserPracticeProgressInLesson returns published practice completion counts scoped to a lesson.
|
||||||
func (s *Store) LmsUserPracticeProgressInLesson(ctx context.Context, userID, lessonID int64) (completed, total int32, err error) {
|
func (s *Store) LmsUserPracticeProgressInLesson(ctx context.Context, userID, lessonID int64) (completed, total int32, err error) {
|
||||||
lessonIDPG := toPgInt8(&lessonID)
|
lessonIDPG := toPgInt8(&lessonID)
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,53 @@ func (s *Store) LinkGoogleAccount(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) LinkAppleAccount(
|
||||||
|
ctx context.Context,
|
||||||
|
userID int64,
|
||||||
|
appleID string,
|
||||||
|
emailVerified bool,
|
||||||
|
) error {
|
||||||
|
return s.queries.LinkAppleAccount(ctx, dbgen.LinkAppleAccountParams{
|
||||||
|
ID: userID,
|
||||||
|
AppleID: pgtype.Text{String: appleID, Valid: true},
|
||||||
|
AppleEmailVerified: pgtype.Bool{Bool: emailVerified, Valid: true},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) CreateAppleUser(
|
||||||
|
ctx context.Context,
|
||||||
|
aUser domain.AppleUser,
|
||||||
|
) (domain.User, error) {
|
||||||
|
res, err := s.queries.CreateAppleUser(ctx, dbgen.CreateAppleUserParams{
|
||||||
|
FirstName: pgtype.Text{String: aUser.GivenName, Valid: aUser.GivenName != ""},
|
||||||
|
LastName: pgtype.Text{String: aUser.FamilyName, Valid: aUser.FamilyName != ""},
|
||||||
|
Email: pgtype.Text{String: aUser.Email, Valid: aUser.Email != ""},
|
||||||
|
AppleID: pgtype.Text{String: aUser.ID, Valid: true},
|
||||||
|
AppleEmailVerified: pgtype.Bool{Bool: aUser.VerifiedEmail, Valid: true},
|
||||||
|
Role: string(domain.RoleStudent),
|
||||||
|
Status: string(domain.UserStatusActive),
|
||||||
|
EmailVerified: aUser.VerifiedEmail,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return domain.User{}, err
|
||||||
|
}
|
||||||
|
return mapDBUser(res, nil, nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetUserByAppleID(
|
||||||
|
ctx context.Context,
|
||||||
|
appleID string,
|
||||||
|
) (domain.User, error) {
|
||||||
|
u, err := s.queries.GetUserByAppleID(ctx, pgtype.Text{String: appleID, Valid: appleID != ""})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return domain.User{}, domain.ErrUserNotFound
|
||||||
|
}
|
||||||
|
return domain.User{}, err
|
||||||
|
}
|
||||||
|
return mapDBUser(u, nil, nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) CreateGoogleUser(
|
func (s *Store) CreateGoogleUser(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
gUser domain.GoogleUser,
|
gUser domain.GoogleUser,
|
||||||
|
|
|
||||||
231
internal/services/authentication/apple.go
Normal file
231
internal/services/authentication/apple.go
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
package authentication
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/MicahParks/keyfunc"
|
||||||
|
"github.com/golang-jwt/jwt/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
const appleJWKSURL = "https://appleid.apple.com/auth/keys"
|
||||||
|
|
||||||
|
var (
|
||||||
|
appleJWKS *keyfunc.JWKS
|
||||||
|
appleJWKSOnce sync.Once
|
||||||
|
appleJWKSErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrAppleSignInNotConfigured = errors.New("apple sign in is not configured")
|
||||||
|
|
||||||
|
func appleAllowedClientIDs(clientIDs string) []string {
|
||||||
|
parts := strings.Split(clientIDs, ",")
|
||||||
|
out := make([]string, 0, len(parts))
|
||||||
|
for _, p := range parts {
|
||||||
|
if id := strings.TrimSpace(p); id != "" {
|
||||||
|
out = append(out, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAppleJWKS() (*keyfunc.JWKS, error) {
|
||||||
|
appleJWKSOnce.Do(func() {
|
||||||
|
appleJWKS, appleJWKSErr = keyfunc.Get(appleJWKSURL, keyfunc.Options{
|
||||||
|
RefreshInterval: time.Hour,
|
||||||
|
RefreshTimeout: 10 * time.Second,
|
||||||
|
RefreshUnknownKID: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return appleJWKS, appleJWKSErr
|
||||||
|
}
|
||||||
|
|
||||||
|
type appleIdentityClaims struct {
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
Email string `json:"email"`
|
||||||
|
EmailVerified any `json:"email_verified"`
|
||||||
|
IsPrivateEmail any `json:"is_private_email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func claimEmailVerified(v any) bool {
|
||||||
|
switch t := v.(type) {
|
||||||
|
case bool:
|
||||||
|
return t
|
||||||
|
case string:
|
||||||
|
return strings.EqualFold(t, "true")
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ValidateAppleIdentityToken(
|
||||||
|
ctx context.Context,
|
||||||
|
idToken string,
|
||||||
|
allowedClientIDs []string,
|
||||||
|
) (domain.AppleUser, error) {
|
||||||
|
if len(allowedClientIDs) == 0 {
|
||||||
|
return domain.AppleUser{}, ErrAppleSignInNotConfigured
|
||||||
|
}
|
||||||
|
if idToken == "" {
|
||||||
|
return domain.AppleUser{}, errors.New("missing apple identity token")
|
||||||
|
}
|
||||||
|
|
||||||
|
jwks, err := getAppleJWKS()
|
||||||
|
if err != nil {
|
||||||
|
return domain.AppleUser{}, fmt.Errorf("failed to load apple jwks: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := jwt.ParseWithClaims(idToken, &appleIdentityClaims{}, jwks.Keyfunc)
|
||||||
|
if err != nil {
|
||||||
|
return domain.AppleUser{}, fmt.Errorf("invalid apple identity token: %w", err)
|
||||||
|
}
|
||||||
|
if !token.Valid {
|
||||||
|
return domain.AppleUser{}, errors.New("invalid apple identity token")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(*appleIdentityClaims)
|
||||||
|
if !ok {
|
||||||
|
return domain.AppleUser{}, errors.New("invalid apple token claims")
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Issuer != "https://appleid.apple.com" {
|
||||||
|
return domain.AppleUser{}, errors.New("invalid apple token issuer")
|
||||||
|
}
|
||||||
|
|
||||||
|
audOK := false
|
||||||
|
for _, aud := range claims.Audience {
|
||||||
|
for _, allowed := range allowedClientIDs {
|
||||||
|
if aud == allowed {
|
||||||
|
audOK = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if audOK {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !audOK {
|
||||||
|
return domain.AppleUser{}, errors.New("apple token audience mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Subject == "" {
|
||||||
|
return domain.AppleUser{}, errors.New("apple token missing subject")
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain.AppleUser{
|
||||||
|
ID: claims.Subject,
|
||||||
|
Email: strings.TrimSpace(claims.Email),
|
||||||
|
VerifiedEmail: claimEmailVerified(claims.EmailVerified),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginWithAppleMobile validates the identity token and signs the user in (iOS / Android / web credential).
|
||||||
|
func (s *Service) LoginWithAppleMobile(
|
||||||
|
ctx context.Context,
|
||||||
|
idToken string,
|
||||||
|
allowedClientIDs string,
|
||||||
|
profile domain.AppleUser,
|
||||||
|
) (domain.LoginSuccess, error) {
|
||||||
|
aUser, err := s.ValidateAppleIdentityToken(ctx, idToken, appleAllowedClientIDs(allowedClientIDs))
|
||||||
|
if err != nil {
|
||||||
|
return domain.LoginSuccess{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if aUser.Email == "" {
|
||||||
|
aUser.Email = strings.TrimSpace(profile.Email)
|
||||||
|
}
|
||||||
|
if aUser.GivenName == "" {
|
||||||
|
aUser.GivenName = strings.TrimSpace(profile.GivenName)
|
||||||
|
}
|
||||||
|
if aUser.FamilyName == "" {
|
||||||
|
aUser.FamilyName = strings.TrimSpace(profile.FamilyName)
|
||||||
|
}
|
||||||
|
if !aUser.VerifiedEmail && profile.VerifiedEmail {
|
||||||
|
aUser.VerifiedEmail = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.LoginWithApple(ctx, aUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) LoginWithApple(
|
||||||
|
ctx context.Context,
|
||||||
|
aUser domain.AppleUser,
|
||||||
|
) (domain.LoginSuccess, error) {
|
||||||
|
if aUser.ID == "" {
|
||||||
|
return domain.LoginSuccess{}, errors.New("missing apple user id")
|
||||||
|
}
|
||||||
|
|
||||||
|
var user domain.User
|
||||||
|
var err error
|
||||||
|
|
||||||
|
user, err = s.userStore.GetUserByAppleID(ctx, aUser.ID)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, domain.ErrUserNotFound) {
|
||||||
|
return domain.LoginSuccess{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if aUser.Email == "" {
|
||||||
|
return domain.LoginSuccess{}, errors.New("email is required on first sign in with apple")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err = s.userStore.GetUserByEmailPhone(ctx, aUser.Email, "")
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, domain.ErrUserNotFound) {
|
||||||
|
return domain.LoginSuccess{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err = s.userStore.CreateAppleUser(ctx, aUser)
|
||||||
|
if err != nil {
|
||||||
|
return domain.LoginSuccess{}, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := s.userStore.LinkAppleAccount(ctx, user.ID, aUser.ID, aUser.VerifiedEmail); err != nil {
|
||||||
|
return domain.LoginSuccess{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Status == domain.UserStatusPending {
|
||||||
|
return domain.LoginSuccess{}, domain.ErrUserNotVerified
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Status == domain.UserStatusSuspended {
|
||||||
|
return domain.LoginSuccess{}, ErrUserSuspended
|
||||||
|
}
|
||||||
|
|
||||||
|
oldToken, err := s.tokenStore.GetRefreshTokenByUserID(ctx, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, ErrRefreshTokenNotFound) {
|
||||||
|
return domain.LoginSuccess{}, err
|
||||||
|
}
|
||||||
|
} else if !oldToken.Revoked {
|
||||||
|
if err := s.tokenStore.RevokeRefreshToken(ctx, oldToken.Token); err != nil {
|
||||||
|
return domain.LoginSuccess{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain.LoginSuccess{
|
||||||
|
UserId: user.ID,
|
||||||
|
Role: user.Role,
|
||||||
|
RfToken: refreshToken,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
@ -314,12 +314,16 @@ func (s *Service) lmsModuleProgress(ctx context.Context, userID, moduleID int64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, false, 0, 0, err
|
return 0, false, 0, 0, err
|
||||||
}
|
}
|
||||||
if len(lessons) == 0 {
|
|
||||||
return 0, false, 0, 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var doneLessons int32
|
var doneLessons int32
|
||||||
for _, lesson := range lessons {
|
for _, lesson := range lessons {
|
||||||
|
practiceCount, err := s.store.LmsCountPublishedPracticesInLesson(ctx, lesson.ID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false, 0, 0, err
|
||||||
|
}
|
||||||
|
if practiceCount == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
total++
|
||||||
lessonFraction, _, _, _, err := s.lmsLessonProgress(ctx, userID, lesson.ID)
|
lessonFraction, _, _, _, err := s.lmsLessonProgress(ctx, userID, lesson.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, false, 0, 0, err
|
return 0, false, 0, 0, err
|
||||||
|
|
@ -328,7 +332,9 @@ func (s *Service) lmsModuleProgress(ctx context.Context, userID, moduleID int64)
|
||||||
doneLessons++
|
doneLessons++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
total = int32(len(lessons))
|
if total == 0 {
|
||||||
|
return 0, false, 0, 0, nil
|
||||||
|
}
|
||||||
fraction = float64(doneLessons) / float64(total)
|
fraction = float64(doneLessons) / float64(total)
|
||||||
return fraction, fraction >= 1, doneLessons, total, nil
|
return fraction, fraction >= 1, doneLessons, total, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,76 @@ func (h *Handler) GoogleAndroidLogin(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AppleLogin godoc
|
||||||
|
// @Summary Login via Sign in with Apple identity token
|
||||||
|
// @Description Validates an Apple identity token (iOS, Android, or web). On first sign-in, include email and name if Apple only returns them to the client once.
|
||||||
|
// @Tags auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body object true "Apple login payload"
|
||||||
|
// @Router /api/v1/auth/apple [post]
|
||||||
|
func (h *Handler) AppleLogin(c *fiber.Ctx) error {
|
||||||
|
var req struct {
|
||||||
|
IDToken string `json:"id_token" validate:"required"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
FirstName string `json:"first_name"`
|
||||||
|
LastName string `json:"last_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid request body",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.Cfg.AppleSignInClientIDs == "" {
|
||||||
|
return c.Status(fiber.StatusServiceUnavailable).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Apple Sign In is not configured",
|
||||||
|
Error: "APPLE_SIGN_IN_CLIENT_IDS is not set",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
loginRes, err := h.authSvc.LoginWithAppleMobile(c.Context(), req.IDToken, h.Cfg.AppleSignInClientIDs, domain.AppleUser{
|
||||||
|
Email: req.Email,
|
||||||
|
GivenName: req.FirstName,
|
||||||
|
FamilyName: req.LastName,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
h.mongoLoggerSvc.Error("Apple login failed",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Int("id_token_length", len(req.IDToken)),
|
||||||
|
)
|
||||||
|
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Apple 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: loginUserRes{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
RefreshToken: loginRes.RfToken,
|
||||||
|
Role: string(loginRes.Role),
|
||||||
|
UserID: loginRes.UserId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// GoogleLogin godoc
|
// GoogleLogin godoc
|
||||||
// @Summary Google login redirect
|
// @Summary Google login redirect
|
||||||
// @Tags auth
|
// @Tags auth
|
||||||
|
|
|
||||||
|
|
@ -291,6 +291,7 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Post("/auth/google/android", h.GoogleAndroidLogin)
|
groupV1.Post("/auth/google/android", h.GoogleAndroidLogin)
|
||||||
groupV1.Get("/auth/google/login", h.GoogleLogin)
|
groupV1.Get("/auth/google/login", h.GoogleLogin)
|
||||||
groupV1.Get("/auth/google/callback", h.GoogleCallback)
|
groupV1.Get("/auth/google/callback", h.GoogleCallback)
|
||||||
|
groupV1.Post("/auth/apple", h.AppleLogin)
|
||||||
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