mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-08-31 01:13:06 +08:00
fix(auth): apply promo codes to oauth signups
This commit is contained in:
@@ -143,6 +143,7 @@ func (h *AuthHandler) DingTalkOAuthStart(c *gin.Context) {
|
||||
|
||||
intent := normalizeOAuthIntent(c.Query("intent"))
|
||||
setDingTalkCookie(c, dingTalkOAuthIntentCookieName, encodeCookieValue(intent), dingTalkOAuthCookieMaxAgeSec, secureCookie)
|
||||
captureOAuthPromoCode(c, secureCookie)
|
||||
|
||||
setOAuthPendingBrowserCookie(c, browserSessionKey, secureCookie)
|
||||
clearOAuthPendingSessionCookie(c, secureCookie)
|
||||
@@ -317,6 +318,7 @@ func (h *AuthHandler) DingTalkOAuthCallback(c *gin.Context) {
|
||||
clearDingTalkCookie(c, dingTalkOAuthStateCookieName, secureCookie)
|
||||
clearDingTalkCookie(c, dingTalkOAuthRedirectCookie, secureCookie)
|
||||
clearDingTalkCookie(c, dingTalkOAuthIntentCookieName, secureCookie)
|
||||
clearOAuthPromoCodeCookie(c, secureCookie)
|
||||
}()
|
||||
|
||||
expectedState, err := readCookieDecoded(c, dingTalkOAuthStateCookieName)
|
||||
@@ -779,7 +781,15 @@ func (h *AuthHandler) CompleteDingTalkOAuthRegistration(c *gin.Context) {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
tokenPair, user, err := h.authService.LoginOrRegisterOAuthWithTokenPair(c.Request.Context(), email, username, req.InvitationCode, req.AffCode, "dingtalk")
|
||||
tokenPair, user, err := h.authService.LoginOrRegisterOAuthWithTokenPairAndPromoCode(
|
||||
c.Request.Context(),
|
||||
email,
|
||||
username,
|
||||
req.InvitationCode,
|
||||
req.AffCode,
|
||||
pendingOAuthPromoCode(session),
|
||||
"dingtalk",
|
||||
)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
|
||||
@@ -78,6 +78,7 @@ func (h *AuthHandler) emailOAuthStart(c *gin.Context, provider string) {
|
||||
emailOAuthSetCookie(c, emailOAuthStateCookieName, encodeCookieValue(state), secureCookie)
|
||||
emailOAuthSetCookie(c, emailOAuthRedirectCookie, encodeCookieValue(redirectTo), secureCookie)
|
||||
emailOAuthSetCookie(c, emailOAuthProviderCookie, encodeCookieValue(provider), secureCookie)
|
||||
captureOAuthPromoCode(c, secureCookie)
|
||||
if affCode := strings.TrimSpace(firstNonEmpty(c.Query("aff_code"), c.Query("aff"))); affCode != "" {
|
||||
emailOAuthSetCookie(c, emailOAuthAffiliateCookie, encodeCookieValue(affCode), secureCookie)
|
||||
} else {
|
||||
@@ -119,6 +120,7 @@ func (h *AuthHandler) emailOAuthCallback(c *gin.Context, provider string) {
|
||||
emailOAuthClearCookie(c, emailOAuthRedirectCookie, secureCookie)
|
||||
emailOAuthClearCookie(c, emailOAuthProviderCookie, secureCookie)
|
||||
emailOAuthClearCookie(c, emailOAuthAffiliateCookie, secureCookie)
|
||||
clearOAuthPromoCodeCookie(c, secureCookie)
|
||||
}()
|
||||
expectedState, err := readCookieDecoded(c, emailOAuthStateCookieName)
|
||||
if err != nil || expectedState == "" || expectedState != state {
|
||||
@@ -181,7 +183,13 @@ func (h *AuthHandler) emailOAuthCallbackWithProfile(
|
||||
return
|
||||
}
|
||||
|
||||
tokenPair, user, err := h.authService.LoginOrRegisterVerifiedEmailOAuthWithInvitation(c.Request.Context(), input, "", affiliateCode)
|
||||
tokenPair, user, err := h.authService.LoginOrRegisterVerifiedEmailOAuthWithSignupCodes(
|
||||
c.Request.Context(),
|
||||
input,
|
||||
"",
|
||||
affiliateCode,
|
||||
readOAuthPromoCode(c),
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrOAuthInvitationRequired) {
|
||||
if pendingErr := h.createEmailOAuthRegistrationPendingSession(c, provider, frontendCallback, redirectTo, profile); pendingErr != nil {
|
||||
@@ -427,6 +435,7 @@ func (h *AuthHandler) completeEmailOAuthRegistration(c *gin.Context, provider st
|
||||
response.ErrorFrom(c, infraerrors.InternalServer("PENDING_AUTH_BIND_APPLY_FAILED", "failed to consume pending oauth session").WithCause(err))
|
||||
return
|
||||
}
|
||||
h.authService.ApplyOAuthSignupPromoCode(c.Request.Context(), user.ID, pendingOAuthPromoCode(session))
|
||||
h.authService.RecordSuccessfulLogin(c.Request.Context(), user.ID)
|
||||
clearCookies()
|
||||
writeOAuthTokenPairResponse(c, tokenPair)
|
||||
|
||||
@@ -184,6 +184,49 @@ func TestEmailOAuthCallbackCreatesPasswordRegistrationSessionForNewEmail(t *test
|
||||
require.Equal(t, "aff-user@example.com", completion["resolved_email"])
|
||||
}
|
||||
|
||||
func TestEmailOAuthStartPreservesPromoCodeInPendingSession(t *testing.T) {
|
||||
handler, client := newOAuthPendingFlowTestHandlerWithDependencies(t, oauthPendingFlowTestHandlerOptions{
|
||||
settingValues: map[string]string{
|
||||
service.SettingKeyGitHubOAuthEnabled: "true",
|
||||
service.SettingKeyGitHubOAuthClientID: "github-client",
|
||||
service.SettingKeyGitHubOAuthClientSecret: "github-secret",
|
||||
service.SettingKeyGitHubOAuthRedirectURL: "https://app.example/api/v1/auth/oauth/github/callback",
|
||||
},
|
||||
})
|
||||
ctx := context.Background()
|
||||
|
||||
startRecorder := httptest.NewRecorder()
|
||||
startCtx, _ := gin.CreateTestContext(startRecorder)
|
||||
startCtx.Request = httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/github/start?promo_code=WELCOME2024", nil)
|
||||
|
||||
handler.GitHubOAuthStart(startCtx)
|
||||
|
||||
require.Equal(t, http.StatusFound, startRecorder.Code)
|
||||
promoCookie := findCookie(startRecorder.Result().Cookies(), oauthPromoCodeCookieName)
|
||||
require.NotNil(t, promoCookie)
|
||||
require.Equal(t, "WELCOME2024", decodeCookieValueForTest(t, promoCookie.Value))
|
||||
|
||||
callbackRecorder := httptest.NewRecorder()
|
||||
callbackCtx, _ := gin.CreateTestContext(callbackRecorder)
|
||||
callbackReq := httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/github/callback", nil)
|
||||
callbackReq.AddCookie(promoCookie)
|
||||
callbackCtx.Request = callbackReq
|
||||
|
||||
handler.emailOAuthCallbackWithProfile(callbackCtx, "github", config.EmailOAuthProviderConfig{
|
||||
FrontendRedirectURL: "/auth/oauth/callback",
|
||||
}, "/auth/oauth/callback", "/dashboard", &emailOAuthProfile{
|
||||
Subject: "github-promo-user",
|
||||
Email: "promo-user@example.com",
|
||||
EmailVerified: true,
|
||||
Username: "promo-user",
|
||||
})
|
||||
|
||||
require.Equal(t, http.StatusFound, callbackRecorder.Code)
|
||||
session, err := client.PendingAuthSession.Query().Only(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "WELCOME2024", pendingOAuthPromoCode(session))
|
||||
}
|
||||
|
||||
func TestCompleteEmailOAuthRegistrationUsesAffiliateCodeFromPendingSession(t *testing.T) {
|
||||
affiliateRepo := newOAuthEmailAffiliateRepoStub(map[string]int64{"AFF456": 2002})
|
||||
handler, client := newOAuthPendingFlowTestHandlerWithDependencies(t, oauthPendingFlowTestHandlerOptions{
|
||||
|
||||
@@ -110,6 +110,7 @@ func (h *AuthHandler) LinuxDoOAuthStart(c *gin.Context) {
|
||||
setCookie(c, linuxDoOAuthRedirectCookie, encodeCookieValue(redirectTo), linuxDoOAuthCookieMaxAgeSec, secureCookie)
|
||||
intent := normalizeOAuthIntent(c.Query("intent"))
|
||||
setCookie(c, linuxDoOAuthIntentCookieName, encodeCookieValue(intent), linuxDoOAuthCookieMaxAgeSec, secureCookie)
|
||||
captureOAuthPromoCode(c, secureCookie)
|
||||
setOAuthPendingBrowserCookie(c, browserSessionKey, secureCookie)
|
||||
clearOAuthPendingSessionCookie(c, secureCookie)
|
||||
if intent == oauthIntentBindCurrentUser {
|
||||
@@ -182,6 +183,7 @@ func (h *AuthHandler) LinuxDoOAuthCallback(c *gin.Context) {
|
||||
clearCookie(c, linuxDoOAuthRedirectCookie, secureCookie)
|
||||
clearCookie(c, linuxDoOAuthIntentCookieName, secureCookie)
|
||||
clearCookie(c, linuxDoOAuthBindUserCookieName, secureCookie)
|
||||
clearOAuthPromoCodeCookie(c, secureCookie)
|
||||
}()
|
||||
|
||||
expectedState, err := readCookieDecoded(c, linuxDoOAuthStateCookieName)
|
||||
@@ -329,7 +331,15 @@ func (h *AuthHandler) LinuxDoOAuthCallback(c *gin.Context) {
|
||||
redirectOAuthError(c, frontendCallback, "session_error", infraerrors.Reason(err), infraerrors.Message(err))
|
||||
return
|
||||
}
|
||||
tokenPair, user, err := h.authService.LoginOrRegisterOAuthWithTokenPair(c.Request.Context(), email, username, "", "", "linuxdo")
|
||||
tokenPair, user, err := h.authService.LoginOrRegisterOAuthWithTokenPairAndPromoCode(
|
||||
c.Request.Context(),
|
||||
email,
|
||||
username,
|
||||
"",
|
||||
"",
|
||||
readOAuthPromoCode(c),
|
||||
"linuxdo",
|
||||
)
|
||||
if err == nil {
|
||||
if err := applyPendingOAuthBinding(
|
||||
c.Request.Context(),
|
||||
@@ -561,7 +571,15 @@ func (h *AuthHandler) CompleteLinuxDoOAuthRegistration(c *gin.Context) {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
tokenPair, user, err := h.authService.LoginOrRegisterOAuthWithTokenPair(c.Request.Context(), email, username, req.InvitationCode, req.AffCode, "linuxdo")
|
||||
tokenPair, user, err := h.authService.LoginOrRegisterOAuthWithTokenPairAndPromoCode(
|
||||
c.Request.Context(),
|
||||
email,
|
||||
username,
|
||||
req.InvitationCode,
|
||||
req.AffCode,
|
||||
pendingOAuthPromoCode(session),
|
||||
"linuxdo",
|
||||
)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
|
||||
@@ -32,10 +32,12 @@ const (
|
||||
oauthPendingBrowserCookieName = "oauth_pending_browser_session"
|
||||
oauthPendingSessionCookiePath = "/api/v1/auth/oauth"
|
||||
oauthPendingSessionCookieName = "oauth_pending_session"
|
||||
oauthPromoCodeCookieName = "oauth_promo_code"
|
||||
oauthPendingCookieMaxAgeSec = 10 * 60
|
||||
oauthPendingChoiceStep = "choose_account_action_required"
|
||||
|
||||
oauthCompletionResponseKey = "completion_response"
|
||||
oauthPromoCodeStateKey = "promo_code"
|
||||
)
|
||||
|
||||
var pendingOAuthCreateAccountPreCommitHook func(context.Context, *dbent.PendingAuthSession) error
|
||||
@@ -161,6 +163,53 @@ func readOAuthPendingSessionCookie(c *gin.Context) (string, error) {
|
||||
return readCookieDecoded(c, oauthPendingSessionCookieName)
|
||||
}
|
||||
|
||||
func captureOAuthPromoCode(c *gin.Context, secure bool) {
|
||||
promoCode := strings.TrimSpace(c.Query("promo_code"))
|
||||
if promoCode == "" {
|
||||
clearOAuthPromoCodeCookie(c, secure)
|
||||
return
|
||||
}
|
||||
http.SetCookie(c.Writer, &http.Cookie{
|
||||
Name: oauthPromoCodeCookieName,
|
||||
Value: encodeCookieValue(promoCode),
|
||||
Path: oauthPendingBrowserCookiePath,
|
||||
MaxAge: oauthPendingCookieMaxAgeSec,
|
||||
HttpOnly: true,
|
||||
Secure: secure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
|
||||
func clearOAuthPromoCodeCookie(c *gin.Context, secure bool) {
|
||||
http.SetCookie(c.Writer, &http.Cookie{
|
||||
Name: oauthPromoCodeCookieName,
|
||||
Value: "",
|
||||
Path: oauthPendingBrowserCookiePath,
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
Secure: secure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
|
||||
func readOAuthPromoCode(c *gin.Context) string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
promoCode, err := readCookieDecoded(c, oauthPromoCodeCookieName)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(promoCode)
|
||||
}
|
||||
|
||||
func pendingOAuthPromoCode(session *dbent.PendingAuthSession) string {
|
||||
if session == nil {
|
||||
return ""
|
||||
}
|
||||
return pendingSessionStringValue(session.LocalFlowState, oauthPromoCodeStateKey)
|
||||
}
|
||||
|
||||
func redirectToFrontendCallback(c *gin.Context, frontendCallback string) {
|
||||
u, err := url.Parse(frontendCallback)
|
||||
if err != nil {
|
||||
@@ -183,6 +232,13 @@ func (h *AuthHandler) createOAuthPendingSession(c *gin.Context, payload oauthPen
|
||||
return err
|
||||
}
|
||||
|
||||
localFlowState := map[string]any{
|
||||
oauthCompletionResponseKey: payload.CompletionResponse,
|
||||
}
|
||||
if promoCode := readOAuthPromoCode(c); promoCode != "" {
|
||||
localFlowState[oauthPromoCodeStateKey] = promoCode
|
||||
}
|
||||
|
||||
session, err := svc.CreatePendingSession(c.Request.Context(), service.CreatePendingAuthSessionInput{
|
||||
Intent: strings.TrimSpace(payload.Intent),
|
||||
Identity: payload.Identity,
|
||||
@@ -191,9 +247,7 @@ func (h *AuthHandler) createOAuthPendingSession(c *gin.Context, payload oauthPen
|
||||
RedirectTo: strings.TrimSpace(payload.RedirectTo),
|
||||
BrowserSessionKey: strings.TrimSpace(payload.BrowserSessionKey),
|
||||
UpstreamIdentityClaims: payload.UpstreamIdentityClaims,
|
||||
LocalFlowState: map[string]any{
|
||||
oauthCompletionResponseKey: payload.CompletionResponse,
|
||||
},
|
||||
LocalFlowState: localFlowState,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("pending auth session create failed",
|
||||
@@ -1820,6 +1874,7 @@ func (h *AuthHandler) createPendingOAuthAccount(c *gin.Context, provider string)
|
||||
return
|
||||
}
|
||||
|
||||
h.authService.ApplyOAuthSignupPromoCode(c.Request.Context(), user.ID, pendingOAuthPromoCode(session))
|
||||
h.authService.RecordSuccessfulLogin(c.Request.Context(), user.ID)
|
||||
// createPendingOAuthAccount = 注册新账户,需要把钉钉昵称同步到 users.username 作为初始值
|
||||
h.maybeSyncDingTalkAfterRegistration(c.Request.Context(), session, user.ID)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -1049,6 +1050,201 @@ func TestCreateOIDCOAuthAccountCreatesUserBindsIdentityAndConsumesSession(t *tes
|
||||
require.NotNil(t, storedSession.ConsumedAt)
|
||||
}
|
||||
|
||||
func TestCreateOIDCOAuthAccountAppliesPromoCodeFromPendingSession(t *testing.T) {
|
||||
promoRepo := newOAuthPendingFlowPromoRepoStub("WELCOME2024", 25)
|
||||
emailCache := &oauthPendingFlowEmailCacheStub{
|
||||
verificationCodes: map[string]*service.VerificationCodeData{
|
||||
"promo@example.com": {
|
||||
Code: "246810",
|
||||
CreatedAt: time.Now().UTC(),
|
||||
ExpiresAt: time.Now().UTC().Add(15 * time.Minute),
|
||||
},
|
||||
},
|
||||
}
|
||||
handler, client := newOAuthPendingFlowTestHandlerWithDependencies(t, oauthPendingFlowTestHandlerOptions{
|
||||
emailVerifyEnabled: true,
|
||||
emailCache: emailCache,
|
||||
promoRepo: promoRepo,
|
||||
settingValues: map[string]string{
|
||||
service.SettingKeyPromoCodeEnabled: "true",
|
||||
},
|
||||
})
|
||||
ctx := context.Background()
|
||||
|
||||
session, err := client.PendingAuthSession.Create().
|
||||
SetSessionToken("promo-create-account-session-token").
|
||||
SetIntent(oauthIntentLogin).
|
||||
SetProviderType("oidc").
|
||||
SetProviderKey("https://issuer.example").
|
||||
SetProviderSubject("oidc-promo-123").
|
||||
SetBrowserSessionKey("promo-create-account-browser-key").
|
||||
SetUpstreamIdentityClaims(map[string]any{"username": "promo_user"}).
|
||||
SetLocalFlowState(map[string]any{oauthPromoCodeStateKey: "WELCOME2024"}).
|
||||
SetExpiresAt(time.Now().UTC().Add(10 * time.Minute)).
|
||||
Save(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
body := bytes.NewBufferString(`{"email":"promo@example.com","verify_code":"246810","password":"secret-123"}`)
|
||||
recorder := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(recorder)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/oauth/oidc/create-account", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.AddCookie(&http.Cookie{Name: oauthPendingSessionCookieName, Value: encodeCookieValue(session.SessionToken)})
|
||||
req.AddCookie(&http.Cookie{Name: oauthPendingBrowserCookieName, Value: encodeCookieValue(session.BrowserSessionKey)})
|
||||
ginCtx.Request = req
|
||||
|
||||
handler.CreateOIDCOAuthAccount(ginCtx)
|
||||
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
require.Equal(t, []string{"WELCOME2024"}, promoRepo.applyCalls)
|
||||
createdUser, err := client.User.Query().Where(dbuser.EmailEQ("promo@example.com")).Only(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 25.0, createdUser.Balance)
|
||||
require.Len(t, promoRepo.usages, 1)
|
||||
require.Equal(t, createdUser.ID, promoRepo.usages[0].UserID)
|
||||
}
|
||||
|
||||
func TestCreateOIDCOAuthAccountWithoutPromoCodeDoesNotApplyPromo(t *testing.T) {
|
||||
promoRepo := newOAuthPendingFlowPromoRepoStub("WELCOME2024", 25)
|
||||
emailCache := &oauthPendingFlowEmailCacheStub{
|
||||
verificationCodes: map[string]*service.VerificationCodeData{
|
||||
"no-promo@example.com": {
|
||||
Code: "246810",
|
||||
CreatedAt: time.Now().UTC(),
|
||||
ExpiresAt: time.Now().UTC().Add(15 * time.Minute),
|
||||
},
|
||||
},
|
||||
}
|
||||
handler, client := newOAuthPendingFlowTestHandlerWithDependencies(t, oauthPendingFlowTestHandlerOptions{
|
||||
emailVerifyEnabled: true,
|
||||
emailCache: emailCache,
|
||||
promoRepo: promoRepo,
|
||||
settingValues: map[string]string{
|
||||
service.SettingKeyPromoCodeEnabled: "true",
|
||||
},
|
||||
})
|
||||
ctx := context.Background()
|
||||
|
||||
session, err := client.PendingAuthSession.Create().
|
||||
SetSessionToken("no-promo-create-account-session-token").
|
||||
SetIntent(oauthIntentLogin).
|
||||
SetProviderType("oidc").
|
||||
SetProviderKey("https://issuer.example").
|
||||
SetProviderSubject("oidc-no-promo-123").
|
||||
SetBrowserSessionKey("no-promo-create-account-browser-key").
|
||||
SetUpstreamIdentityClaims(map[string]any{"username": "no_promo_user"}).
|
||||
SetExpiresAt(time.Now().UTC().Add(10 * time.Minute)).
|
||||
Save(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
body := bytes.NewBufferString(`{"email":"no-promo@example.com","verify_code":"246810","password":"secret-123"}`)
|
||||
recorder := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(recorder)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/oauth/oidc/create-account", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.AddCookie(&http.Cookie{Name: oauthPendingSessionCookieName, Value: encodeCookieValue(session.SessionToken)})
|
||||
req.AddCookie(&http.Cookie{Name: oauthPendingBrowserCookieName, Value: encodeCookieValue(session.BrowserSessionKey)})
|
||||
ginCtx.Request = req
|
||||
|
||||
handler.CreateOIDCOAuthAccount(ginCtx)
|
||||
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
require.Empty(t, promoRepo.applyCalls)
|
||||
createdUser, err := client.User.Query().Where(dbuser.EmailEQ("no-promo@example.com")).Only(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, createdUser.Balance)
|
||||
}
|
||||
|
||||
func TestCreateOIDCOAuthAccountDoesNotApplyPromoWhenDisabled(t *testing.T) {
|
||||
promoRepo := newOAuthPendingFlowPromoRepoStub("WELCOME2024", 25)
|
||||
emailCache := &oauthPendingFlowEmailCacheStub{
|
||||
verificationCodes: map[string]*service.VerificationCodeData{
|
||||
"promo-disabled@example.com": {
|
||||
Code: "246810",
|
||||
CreatedAt: time.Now().UTC(),
|
||||
ExpiresAt: time.Now().UTC().Add(15 * time.Minute),
|
||||
},
|
||||
},
|
||||
}
|
||||
handler, client := newOAuthPendingFlowTestHandlerWithDependencies(t, oauthPendingFlowTestHandlerOptions{
|
||||
emailVerifyEnabled: true,
|
||||
emailCache: emailCache,
|
||||
promoRepo: promoRepo,
|
||||
settingValues: map[string]string{
|
||||
service.SettingKeyPromoCodeEnabled: "false",
|
||||
},
|
||||
})
|
||||
ctx := context.Background()
|
||||
|
||||
session, err := client.PendingAuthSession.Create().
|
||||
SetSessionToken("promo-disabled-session-token").
|
||||
SetIntent(oauthIntentLogin).
|
||||
SetProviderType("oidc").
|
||||
SetProviderKey("https://issuer.example").
|
||||
SetProviderSubject("oidc-promo-disabled-123").
|
||||
SetBrowserSessionKey("promo-disabled-browser-key").
|
||||
SetUpstreamIdentityClaims(map[string]any{"username": "promo_disabled_user"}).
|
||||
SetLocalFlowState(map[string]any{oauthPromoCodeStateKey: "WELCOME2024"}).
|
||||
SetExpiresAt(time.Now().UTC().Add(10 * time.Minute)).
|
||||
Save(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
body := bytes.NewBufferString(`{"email":"promo-disabled@example.com","verify_code":"246810","password":"secret-123"}`)
|
||||
recorder := httptest.NewRecorder()
|
||||
ginCtx, _ := gin.CreateTestContext(recorder)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/oauth/oidc/create-account", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.AddCookie(&http.Cookie{Name: oauthPendingSessionCookieName, Value: encodeCookieValue(session.SessionToken)})
|
||||
req.AddCookie(&http.Cookie{Name: oauthPendingBrowserCookieName, Value: encodeCookieValue(session.BrowserSessionKey)})
|
||||
ginCtx.Request = req
|
||||
|
||||
handler.CreateOIDCOAuthAccount(ginCtx)
|
||||
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
require.Empty(t, promoRepo.applyCalls)
|
||||
createdUser, err := client.User.Query().Where(dbuser.EmailEQ("promo-disabled@example.com")).Only(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, createdUser.Balance)
|
||||
}
|
||||
|
||||
func TestOAuthExistingUserLoginDoesNotApplyPromoCode(t *testing.T) {
|
||||
promoRepo := newOAuthPendingFlowPromoRepoStub("WELCOME2024", 25)
|
||||
handler, client := newOAuthPendingFlowTestHandlerWithDependencies(t, oauthPendingFlowTestHandlerOptions{
|
||||
promoRepo: promoRepo,
|
||||
settingValues: map[string]string{
|
||||
service.SettingKeyPromoCodeEnabled: "true",
|
||||
},
|
||||
})
|
||||
ctx := context.Background()
|
||||
|
||||
existingUser, err := client.User.Create().
|
||||
SetEmail("existing-promo@example.com").
|
||||
SetUsername("existing").
|
||||
SetPasswordHash("hash").
|
||||
SetRole(service.RoleUser).
|
||||
SetStatus(service.StatusActive).
|
||||
SetBalance(7).
|
||||
Save(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, loggedInUser, err := handler.authService.LoginOrRegisterOAuthWithTokenPairAndPromoCode(
|
||||
ctx,
|
||||
existingUser.Email,
|
||||
existingUser.Username,
|
||||
"",
|
||||
"",
|
||||
"WELCOME2024",
|
||||
"oidc",
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, existingUser.ID, loggedInUser.ID)
|
||||
require.Empty(t, promoRepo.applyCalls)
|
||||
reloadedUser, err := client.User.Get(ctx, existingUser.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 7.0, reloadedUser.Balance)
|
||||
}
|
||||
|
||||
func TestCreateOIDCOAuthAccountExistingEmailReturnsChoicePendingSessionState(t *testing.T) {
|
||||
handler, client := newOAuthPendingFlowTestHandlerWithEmailVerification(t, false, "owner@example.com", "135790")
|
||||
ctx := context.Background()
|
||||
@@ -2120,6 +2316,7 @@ type oauthPendingFlowTestHandlerOptions struct {
|
||||
emailVerifyEnabled bool
|
||||
emailCache service.EmailCache
|
||||
settingValues map[string]string
|
||||
promoRepo service.PromoCodeRepository
|
||||
defaultSubAssigner service.DefaultSubscriptionAssigner
|
||||
affiliateService *service.AffiliateService
|
||||
affiliateFactory func(*dbent.Client, *service.SettingService) *service.AffiliateService
|
||||
@@ -2212,6 +2409,10 @@ CREATE TABLE IF NOT EXISTS user_affiliates (
|
||||
options: options.userRepoOptions,
|
||||
}
|
||||
redeemRepo := &oauthPendingFlowRedeemCodeRepo{client: client}
|
||||
var promoService *service.PromoService
|
||||
if options.promoRepo != nil {
|
||||
promoService = service.NewPromoService(options.promoRepo, userRepo, nil, client, nil)
|
||||
}
|
||||
var emailService *service.EmailService
|
||||
if options.emailCache != nil {
|
||||
emailService = service.NewEmailService(&oauthPendingFlowSettingRepoStub{
|
||||
@@ -2230,7 +2431,7 @@ CREATE TABLE IF NOT EXISTS user_affiliates (
|
||||
emailService,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
promoService,
|
||||
options.defaultSubAssigner,
|
||||
affiliateService,
|
||||
nil,
|
||||
@@ -2250,10 +2451,11 @@ CREATE TABLE IF NOT EXISTS user_affiliates (
|
||||
}
|
||||
|
||||
return &AuthHandler{
|
||||
authService: authSvc,
|
||||
userService: userSvc,
|
||||
settingSvc: settingSvc,
|
||||
totpService: totpSvc,
|
||||
authService: authSvc,
|
||||
userService: userSvc,
|
||||
settingSvc: settingSvc,
|
||||
promoService: promoService,
|
||||
totpService: totpSvc,
|
||||
}, client
|
||||
}
|
||||
|
||||
@@ -2272,6 +2474,84 @@ type oauthPendingFlowSettingRepoStub struct {
|
||||
values map[string]string
|
||||
}
|
||||
|
||||
type oauthPendingFlowPromoRepoStub struct {
|
||||
promo *service.PromoCode
|
||||
applyCalls []string
|
||||
usages []service.PromoCodeUsage
|
||||
}
|
||||
|
||||
func newOAuthPendingFlowPromoRepoStub(code string, bonusAmount float64) *oauthPendingFlowPromoRepoStub {
|
||||
return &oauthPendingFlowPromoRepoStub{
|
||||
promo: &service.PromoCode{
|
||||
ID: 1,
|
||||
Code: code,
|
||||
BonusAmount: bonusAmount,
|
||||
Status: service.PromoCodeStatusActive,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *oauthPendingFlowPromoRepoStub) Create(context.Context, *service.PromoCode) error {
|
||||
panic("unexpected Create call")
|
||||
}
|
||||
|
||||
func (r *oauthPendingFlowPromoRepoStub) GetByID(context.Context, int64) (*service.PromoCode, error) {
|
||||
panic("unexpected GetByID call")
|
||||
}
|
||||
|
||||
func (r *oauthPendingFlowPromoRepoStub) GetByCode(_ context.Context, code string) (*service.PromoCode, error) {
|
||||
if r.promo == nil || !strings.EqualFold(strings.TrimSpace(code), r.promo.Code) {
|
||||
return nil, service.ErrPromoCodeNotFound
|
||||
}
|
||||
clone := *r.promo
|
||||
return &clone, nil
|
||||
}
|
||||
|
||||
func (r *oauthPendingFlowPromoRepoStub) GetByCodeForUpdate(ctx context.Context, code string) (*service.PromoCode, error) {
|
||||
promoCode, err := r.GetByCode(ctx, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.applyCalls = append(r.applyCalls, strings.TrimSpace(code))
|
||||
return promoCode, nil
|
||||
}
|
||||
|
||||
func (r *oauthPendingFlowPromoRepoStub) Update(context.Context, *service.PromoCode) error {
|
||||
panic("unexpected Update call")
|
||||
}
|
||||
|
||||
func (r *oauthPendingFlowPromoRepoStub) Delete(context.Context, int64) error {
|
||||
panic("unexpected Delete call")
|
||||
}
|
||||
|
||||
func (r *oauthPendingFlowPromoRepoStub) List(context.Context, pagination.PaginationParams) ([]service.PromoCode, *pagination.PaginationResult, error) {
|
||||
panic("unexpected List call")
|
||||
}
|
||||
|
||||
func (r *oauthPendingFlowPromoRepoStub) ListWithFilters(context.Context, pagination.PaginationParams, string, string) ([]service.PromoCode, *pagination.PaginationResult, error) {
|
||||
panic("unexpected ListWithFilters call")
|
||||
}
|
||||
|
||||
func (r *oauthPendingFlowPromoRepoStub) CreateUsage(_ context.Context, usage *service.PromoCodeUsage) error {
|
||||
r.usages = append(r.usages, *usage)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *oauthPendingFlowPromoRepoStub) GetUsageByPromoCodeAndUser(context.Context, int64, int64) (*service.PromoCodeUsage, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *oauthPendingFlowPromoRepoStub) ListUsagesByPromoCode(context.Context, int64, pagination.PaginationParams) ([]service.PromoCodeUsage, *pagination.PaginationResult, error) {
|
||||
panic("unexpected ListUsagesByPromoCode call")
|
||||
}
|
||||
|
||||
func (r *oauthPendingFlowPromoRepoStub) IncrementUsedCount(context.Context, int64) error {
|
||||
if r.promo != nil {
|
||||
r.promo.UsedCount++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *oauthPendingFlowSettingRepoStub) Get(context.Context, string) (*service.Setting, error) {
|
||||
return nil, service.ErrSettingNotFound
|
||||
}
|
||||
@@ -2813,8 +3093,12 @@ func (r *oauthPendingFlowUserRepo) ListWithFilters(context.Context, pagination.P
|
||||
panic("unexpected ListWithFilters call")
|
||||
}
|
||||
|
||||
func (r *oauthPendingFlowUserRepo) UpdateBalance(context.Context, int64, float64) error {
|
||||
panic("unexpected UpdateBalance call")
|
||||
func (r *oauthPendingFlowUserRepo) UpdateBalance(ctx context.Context, userID int64, amount float64) error {
|
||||
client := r.client
|
||||
if tx := dbent.TxFromContext(ctx); tx != nil {
|
||||
client = tx.Client()
|
||||
}
|
||||
return client.User.UpdateOneID(userID).AddBalance(amount).Exec(ctx)
|
||||
}
|
||||
|
||||
func (r *oauthPendingFlowUserRepo) DeductBalance(context.Context, int64, float64) error {
|
||||
|
||||
@@ -143,6 +143,7 @@ func (h *AuthHandler) OIDCOAuthStart(c *gin.Context) {
|
||||
oidcSetCookie(c, oidcOAuthRedirectCookie, encodeCookieValue(redirectTo), oidcOAuthCookieMaxAgeSec, secureCookie)
|
||||
intent := normalizeOAuthIntent(c.Query("intent"))
|
||||
oidcSetCookie(c, oidcOAuthIntentCookieName, encodeCookieValue(intent), oidcOAuthCookieMaxAgeSec, secureCookie)
|
||||
captureOAuthPromoCode(c, secureCookie)
|
||||
setOAuthPendingBrowserCookie(c, browserSessionKey, secureCookie)
|
||||
clearOAuthPendingSessionCookie(c, secureCookie)
|
||||
if intent == oauthIntentBindCurrentUser {
|
||||
@@ -226,6 +227,7 @@ func (h *AuthHandler) OIDCOAuthCallback(c *gin.Context) {
|
||||
oidcClearCookie(c, oidcOAuthNonceCookie, secureCookie)
|
||||
oidcClearCookie(c, oidcOAuthIntentCookieName, secureCookie)
|
||||
oidcClearCookie(c, oidcOAuthBindUserCookieName, secureCookie)
|
||||
clearOAuthPromoCodeCookie(c, secureCookie)
|
||||
}()
|
||||
|
||||
expectedState, err := readCookieDecoded(c, oidcOAuthStateCookieName)
|
||||
@@ -685,7 +687,15 @@ func (h *AuthHandler) CompleteOIDCOAuthRegistration(c *gin.Context) {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
tokenPair, user, err := h.authService.LoginOrRegisterOAuthWithTokenPair(c.Request.Context(), email, username, req.InvitationCode, req.AffCode, "oidc")
|
||||
tokenPair, user, err := h.authService.LoginOrRegisterOAuthWithTokenPairAndPromoCode(
|
||||
c.Request.Context(),
|
||||
email,
|
||||
username,
|
||||
req.InvitationCode,
|
||||
req.AffCode,
|
||||
pendingOAuthPromoCode(session),
|
||||
"oidc",
|
||||
)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
@@ -1258,7 +1268,13 @@ func (h *AuthHandler) tryOIDCVerifiedEmailFastPath(
|
||||
AvatarURL: pendingSessionStringValue(upstreamClaims, "suggested_avatar_url"),
|
||||
UpstreamMetadata: upstreamMetadata,
|
||||
}
|
||||
tokenPair, _, err := h.authService.LoginOrRegisterVerifiedEmailOAuthWithInvitation(ctx, input, "", "")
|
||||
tokenPair, _, err := h.authService.LoginOrRegisterVerifiedEmailOAuthWithSignupCodes(
|
||||
ctx,
|
||||
input,
|
||||
"",
|
||||
"",
|
||||
readOAuthPromoCode(c),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("[OIDC OAuth] verified-email fast path skipped: reason=%s", infraerrors.Reason(err))
|
||||
return false
|
||||
|
||||
@@ -125,6 +125,7 @@ func (h *AuthHandler) WeChatOAuthStart(c *gin.Context) {
|
||||
wechatSetCookie(c, wechatOAuthRedirectCookieName, encodeCookieValue(redirectTo), wechatOAuthCookieMaxAgeSec, secureCookie)
|
||||
wechatSetCookie(c, wechatOAuthIntentCookieName, encodeCookieValue(intent), wechatOAuthCookieMaxAgeSec, secureCookie)
|
||||
wechatSetCookie(c, wechatOAuthModeCookieName, encodeCookieValue(cfg.mode), wechatOAuthCookieMaxAgeSec, secureCookie)
|
||||
captureOAuthPromoCode(c, secureCookie)
|
||||
setOAuthPendingBrowserCookie(c, browserSessionKey, secureCookie)
|
||||
clearOAuthPendingSessionCookie(c, secureCookie)
|
||||
if intent == oauthIntentBindCurrentUser {
|
||||
@@ -171,6 +172,7 @@ func (h *AuthHandler) WeChatOAuthCallback(c *gin.Context) {
|
||||
wechatClearCookie(c, wechatOAuthIntentCookieName, secureCookie)
|
||||
wechatClearCookie(c, wechatOAuthModeCookieName, secureCookie)
|
||||
wechatClearCookie(c, wechatOAuthBindUserCookieName, secureCookie)
|
||||
clearOAuthPromoCodeCookie(c, secureCookie)
|
||||
}()
|
||||
|
||||
expectedState, err := readCookieDecoded(c, wechatOAuthStateCookieName)
|
||||
@@ -548,7 +550,15 @@ func (h *AuthHandler) CompleteWeChatOAuthRegistration(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
tokenPair, user, err := h.authService.LoginOrRegisterOAuthWithTokenPair(c.Request.Context(), email, username, req.InvitationCode, req.AffCode, "wechat")
|
||||
tokenPair, user, err := h.authService.LoginOrRegisterOAuthWithTokenPairAndPromoCode(
|
||||
c.Request.Context(),
|
||||
email,
|
||||
username,
|
||||
req.InvitationCode,
|
||||
req.AffCode,
|
||||
pendingOAuthPromoCode(session),
|
||||
"wechat",
|
||||
)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
|
||||
@@ -26,7 +26,7 @@ type EmailOAuthIdentityInput struct {
|
||||
}
|
||||
|
||||
func (s *AuthService) LoginOrRegisterVerifiedEmailOAuth(ctx context.Context, input EmailOAuthIdentityInput) (*TokenPair, *User, error) {
|
||||
return s.loginOrRegisterVerifiedEmailOAuth(ctx, input, "", "")
|
||||
return s.loginOrRegisterVerifiedEmailOAuth(ctx, input, "", "", "")
|
||||
}
|
||||
|
||||
func (s *AuthService) LoginOrRegisterVerifiedEmailOAuthWithInvitation(
|
||||
@@ -35,7 +35,17 @@ func (s *AuthService) LoginOrRegisterVerifiedEmailOAuthWithInvitation(
|
||||
invitationCode string,
|
||||
affiliateCode string,
|
||||
) (*TokenPair, *User, error) {
|
||||
return s.loginOrRegisterVerifiedEmailOAuth(ctx, input, invitationCode, affiliateCode)
|
||||
return s.loginOrRegisterVerifiedEmailOAuth(ctx, input, invitationCode, affiliateCode, "")
|
||||
}
|
||||
|
||||
func (s *AuthService) LoginOrRegisterVerifiedEmailOAuthWithSignupCodes(
|
||||
ctx context.Context,
|
||||
input EmailOAuthIdentityInput,
|
||||
invitationCode string,
|
||||
affiliateCode string,
|
||||
promoCode string,
|
||||
) (*TokenPair, *User, error) {
|
||||
return s.loginOrRegisterVerifiedEmailOAuth(ctx, input, invitationCode, affiliateCode, promoCode)
|
||||
}
|
||||
|
||||
func (s *AuthService) loginOrRegisterVerifiedEmailOAuth(
|
||||
@@ -43,6 +53,7 @@ func (s *AuthService) loginOrRegisterVerifiedEmailOAuth(
|
||||
input EmailOAuthIdentityInput,
|
||||
invitationCode string,
|
||||
affiliateCode string,
|
||||
promoCode string,
|
||||
) (*TokenPair, *User, error) {
|
||||
if s == nil || s.userRepo == nil || s.entClient == nil {
|
||||
return nil, nil, ErrServiceUnavailable
|
||||
@@ -131,6 +142,8 @@ func (s *AuthService) loginOrRegisterVerifiedEmailOAuth(
|
||||
if err := s.ApplyProviderDefaultSettingsOnFirstBind(ctx, user.ID, providerType); err != nil {
|
||||
logger.LegacyPrintf("service.auth", "[Auth] Failed to apply %s first bind defaults: %v", providerType, err)
|
||||
}
|
||||
} else {
|
||||
user = s.applyOAuthSignupPromoCode(ctx, user, promoCode)
|
||||
}
|
||||
s.RecordSuccessfulLogin(ctx, user.ID)
|
||||
|
||||
|
||||
@@ -587,6 +587,17 @@ func (s *AuthService) canBypassRegistrationDisabledForOAuth(ctx context.Context,
|
||||
// affiliateCode 用于邀请返利绑定,仅在新用户注册时使用。
|
||||
// signupSource 标识来源渠道("dingtalk"/"linuxdo"/"wechat"/"oidc" 等),仅用于豁免检查。
|
||||
func (s *AuthService) LoginOrRegisterOAuthWithTokenPair(ctx context.Context, email, username, invitationCode, affiliateCode, signupSource string) (*TokenPair, *User, error) {
|
||||
return s.loginOrRegisterOAuthWithTokenPair(ctx, email, username, invitationCode, affiliateCode, "", signupSource)
|
||||
}
|
||||
|
||||
// LoginOrRegisterOAuthWithTokenPairAndPromoCode behaves like
|
||||
// LoginOrRegisterOAuthWithTokenPair and applies promoCode only when a new user
|
||||
// is created.
|
||||
func (s *AuthService) LoginOrRegisterOAuthWithTokenPairAndPromoCode(ctx context.Context, email, username, invitationCode, affiliateCode, promoCode, signupSource string) (*TokenPair, *User, error) {
|
||||
return s.loginOrRegisterOAuthWithTokenPair(ctx, email, username, invitationCode, affiliateCode, promoCode, signupSource)
|
||||
}
|
||||
|
||||
func (s *AuthService) loginOrRegisterOAuthWithTokenPair(ctx context.Context, email, username, invitationCode, affiliateCode, promoCode, signupSource string) (*TokenPair, *User, error) {
|
||||
// 检查 refreshTokenCache 是否可用
|
||||
if s.refreshTokenCache == nil {
|
||||
return nil, nil, errors.New("refresh token cache not configured")
|
||||
@@ -606,6 +617,7 @@ func (s *AuthService) LoginOrRegisterOAuthWithTokenPair(ctx context.Context, ema
|
||||
}
|
||||
|
||||
user, err := s.userRepo.GetByEmail(ctx, email)
|
||||
created := false
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrUserNotFound) {
|
||||
// OAuth 首次登录视为注册
|
||||
@@ -691,6 +703,7 @@ func (s *AuthService) LoginOrRegisterOAuthWithTokenPair(ctx context.Context, ema
|
||||
return nil, nil, ErrServiceUnavailable
|
||||
}
|
||||
user = newUser
|
||||
created = true
|
||||
s.postAuthUserBootstrap(ctx, user, signupSource, false)
|
||||
s.assignSubscriptions(ctx, user.ID, grantPlan.Subscriptions, "auto assigned by signup defaults")
|
||||
// snapshot user × platform quota(fail-open)
|
||||
@@ -711,6 +724,7 @@ func (s *AuthService) LoginOrRegisterOAuthWithTokenPair(ctx context.Context, ema
|
||||
}
|
||||
} else {
|
||||
user = newUser
|
||||
created = true
|
||||
s.postAuthUserBootstrap(ctx, user, signupSource, false)
|
||||
s.assignSubscriptions(ctx, user.ID, grantPlan.Subscriptions, "auto assigned by signup defaults")
|
||||
// snapshot user × platform quota(fail-open)
|
||||
@@ -739,6 +753,9 @@ func (s *AuthService) LoginOrRegisterOAuthWithTokenPair(ctx context.Context, ema
|
||||
logger.LegacyPrintf("service.auth", "[Auth] Failed to update username after oauth login: %v", err)
|
||||
}
|
||||
}
|
||||
if created {
|
||||
user = s.applyOAuthSignupPromoCode(ctx, user, promoCode)
|
||||
}
|
||||
tokenPair, err := s.GenerateTokenPair(ctx, user, "")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("generate token pair: %w", err)
|
||||
@@ -746,6 +763,28 @@ func (s *AuthService) LoginOrRegisterOAuthWithTokenPair(ctx context.Context, ema
|
||||
return tokenPair, user, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) ApplyOAuthSignupPromoCode(ctx context.Context, userID int64, promoCode string) {
|
||||
if userID <= 0 {
|
||||
return
|
||||
}
|
||||
s.applyOAuthSignupPromoCode(ctx, &User{ID: userID}, promoCode)
|
||||
}
|
||||
|
||||
func (s *AuthService) applyOAuthSignupPromoCode(ctx context.Context, user *User, promoCode string) *User {
|
||||
promoCode = strings.TrimSpace(promoCode)
|
||||
if user == nil || user.ID <= 0 || promoCode == "" || s.promoService == nil || s.settingService == nil || !s.settingService.IsPromoCodeEnabled(ctx) {
|
||||
return user
|
||||
}
|
||||
if err := s.promoService.ApplyPromoCode(ctx, user.ID, promoCode); err != nil {
|
||||
logger.LegacyPrintf("service.auth", "[Auth] Failed to apply promo code for oauth user %d: %v", user.ID, err)
|
||||
return user
|
||||
}
|
||||
if updatedUser, err := s.userRepo.GetByID(ctx, user.ID); err == nil {
|
||||
return updatedUser
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func (s *AuthService) assignSubscriptions(ctx context.Context, userID int64, items []DefaultSubscriptionSetting, notes string) {
|
||||
if s.settingService == nil || s.defaultSubAssigner == nil || userID <= 0 {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user