fix(settings): restore wechat and payment config persistence

This commit is contained in:
IanShaw027
2026-04-21 17:35:12 +08:00
parent d08757ce9e
commit ee3f158f4e
19 changed files with 7160 additions and 4473 deletions

View File

@@ -122,6 +122,13 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
LinuxDoConnectClientID: settings.LinuxDoConnectClientID,
LinuxDoConnectClientSecretConfigured: settings.LinuxDoConnectClientSecretConfigured,
LinuxDoConnectRedirectURL: settings.LinuxDoConnectRedirectURL,
WeChatConnectEnabled: settings.WeChatConnectEnabled,
WeChatConnectAppID: settings.WeChatConnectAppID,
WeChatConnectAppSecretConfigured: settings.WeChatConnectAppSecretConfigured,
WeChatConnectMode: settings.WeChatConnectMode,
WeChatConnectScopes: settings.WeChatConnectScopes,
WeChatConnectRedirectURL: settings.WeChatConnectRedirectURL,
WeChatConnectFrontendRedirectURL: settings.WeChatConnectFrontendRedirectURL,
OIDCConnectEnabled: settings.OIDCConnectEnabled,
OIDCConnectProviderName: settings.OIDCConnectProviderName,
OIDCConnectClientID: settings.OIDCConnectClientID,
@@ -246,6 +253,15 @@ type UpdateSettingsRequest struct {
LinuxDoConnectClientSecret string `json:"linuxdo_connect_client_secret"`
LinuxDoConnectRedirectURL string `json:"linuxdo_connect_redirect_url"`
// WeChat Connect OAuth 登录
WeChatConnectEnabled bool `json:"wechat_connect_enabled"`
WeChatConnectAppID string `json:"wechat_connect_app_id"`
WeChatConnectAppSecret string `json:"wechat_connect_app_secret"`
WeChatConnectMode string `json:"wechat_connect_mode"`
WeChatConnectScopes string `json:"wechat_connect_scopes"`
WeChatConnectRedirectURL string `json:"wechat_connect_redirect_url"`
WeChatConnectFrontendRedirectURL string `json:"wechat_connect_frontend_redirect_url"`
// Generic OIDC OAuth 登录
OIDCConnectEnabled bool `json:"oidc_connect_enabled"`
OIDCConnectProviderName string `json:"oidc_connect_provider_name"`
@@ -509,6 +525,54 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
}
if req.WeChatConnectEnabled {
req.WeChatConnectAppID = strings.TrimSpace(req.WeChatConnectAppID)
req.WeChatConnectAppSecret = strings.TrimSpace(req.WeChatConnectAppSecret)
req.WeChatConnectMode = strings.ToLower(strings.TrimSpace(req.WeChatConnectMode))
req.WeChatConnectScopes = strings.TrimSpace(req.WeChatConnectScopes)
req.WeChatConnectRedirectURL = strings.TrimSpace(req.WeChatConnectRedirectURL)
req.WeChatConnectFrontendRedirectURL = strings.TrimSpace(req.WeChatConnectFrontendRedirectURL)
if req.WeChatConnectAppID == "" {
response.BadRequest(c, "WeChat App ID is required when enabled")
return
}
if req.WeChatConnectAppSecret == "" {
if previousSettings.WeChatConnectAppSecret == "" {
response.BadRequest(c, "WeChat App Secret is required when enabled")
return
}
req.WeChatConnectAppSecret = previousSettings.WeChatConnectAppSecret
}
if req.WeChatConnectMode == "" {
req.WeChatConnectMode = "open"
}
switch req.WeChatConnectMode {
case "open", "mp":
default:
response.BadRequest(c, "WeChat mode must be open or mp")
return
}
if req.WeChatConnectScopes == "" {
req.WeChatConnectScopes = service.DefaultWeChatConnectScopesForMode(req.WeChatConnectMode)
}
if req.WeChatConnectRedirectURL == "" {
response.BadRequest(c, "WeChat Redirect URL is required when enabled")
return
}
if err := config.ValidateAbsoluteHTTPURL(req.WeChatConnectRedirectURL); err != nil {
response.BadRequest(c, "WeChat Redirect URL must be an absolute http(s) URL")
return
}
if req.WeChatConnectFrontendRedirectURL == "" {
req.WeChatConnectFrontendRedirectURL = "/auth/wechat/callback"
}
if err := config.ValidateFrontendRedirectURL(req.WeChatConnectFrontendRedirectURL); err != nil {
response.BadRequest(c, "WeChat Frontend Redirect URL is invalid")
return
}
}
// Generic OIDC 参数验证
if req.OIDCConnectEnabled {
req.OIDCConnectProviderName = strings.TrimSpace(req.OIDCConnectProviderName)
@@ -857,6 +921,13 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
LinuxDoConnectClientID: req.LinuxDoConnectClientID,
LinuxDoConnectClientSecret: req.LinuxDoConnectClientSecret,
LinuxDoConnectRedirectURL: req.LinuxDoConnectRedirectURL,
WeChatConnectEnabled: req.WeChatConnectEnabled,
WeChatConnectAppID: req.WeChatConnectAppID,
WeChatConnectAppSecret: req.WeChatConnectAppSecret,
WeChatConnectMode: req.WeChatConnectMode,
WeChatConnectScopes: req.WeChatConnectScopes,
WeChatConnectRedirectURL: req.WeChatConnectRedirectURL,
WeChatConnectFrontendRedirectURL: req.WeChatConnectFrontendRedirectURL,
OIDCConnectEnabled: req.OIDCConnectEnabled,
OIDCConnectProviderName: req.OIDCConnectProviderName,
OIDCConnectClientID: req.OIDCConnectClientID,
@@ -1136,6 +1207,13 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
LinuxDoConnectClientID: updatedSettings.LinuxDoConnectClientID,
LinuxDoConnectClientSecretConfigured: updatedSettings.LinuxDoConnectClientSecretConfigured,
LinuxDoConnectRedirectURL: updatedSettings.LinuxDoConnectRedirectURL,
WeChatConnectEnabled: updatedSettings.WeChatConnectEnabled,
WeChatConnectAppID: updatedSettings.WeChatConnectAppID,
WeChatConnectAppSecretConfigured: updatedSettings.WeChatConnectAppSecretConfigured,
WeChatConnectMode: updatedSettings.WeChatConnectMode,
WeChatConnectScopes: updatedSettings.WeChatConnectScopes,
WeChatConnectRedirectURL: updatedSettings.WeChatConnectRedirectURL,
WeChatConnectFrontendRedirectURL: updatedSettings.WeChatConnectFrontendRedirectURL,
OIDCConnectEnabled: updatedSettings.OIDCConnectEnabled,
OIDCConnectProviderName: updatedSettings.OIDCConnectProviderName,
OIDCConnectClientID: updatedSettings.OIDCConnectClientID,
@@ -1329,6 +1407,27 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if before.LinuxDoConnectRedirectURL != after.LinuxDoConnectRedirectURL {
changed = append(changed, "linuxdo_connect_redirect_url")
}
if before.WeChatConnectEnabled != after.WeChatConnectEnabled {
changed = append(changed, "wechat_connect_enabled")
}
if before.WeChatConnectAppID != after.WeChatConnectAppID {
changed = append(changed, "wechat_connect_app_id")
}
if req.WeChatConnectAppSecret != "" {
changed = append(changed, "wechat_connect_app_secret")
}
if before.WeChatConnectMode != after.WeChatConnectMode {
changed = append(changed, "wechat_connect_mode")
}
if before.WeChatConnectScopes != after.WeChatConnectScopes {
changed = append(changed, "wechat_connect_scopes")
}
if before.WeChatConnectRedirectURL != after.WeChatConnectRedirectURL {
changed = append(changed, "wechat_connect_redirect_url")
}
if before.WeChatConnectFrontendRedirectURL != after.WeChatConnectFrontendRedirectURL {
changed = append(changed, "wechat_connect_frontend_redirect_url")
}
if before.OIDCConnectEnabled != after.OIDCConnectEnabled {
changed = append(changed, "oidc_connect_enabled")
}

View File

@@ -8,7 +8,6 @@ import (
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
@@ -149,7 +148,7 @@ func (h *AuthHandler) WeChatOAuthStart(c *gin.Context) {
// WeChatOAuthCallback exchanges the code with WeChat, resolves openid/unionid,
// and stores the result in the unified pending-auth flow.
func (h *AuthHandler) WeChatOAuthCallback(c *gin.Context) {
frontendCallback := wechatOAuthFrontendCallback()
frontendCallback := h.wechatOAuthFrontendCallback(c.Request.Context())
if providerErr := strings.TrimSpace(c.Query("error")); providerErr != "" {
redirectOAuthError(c, frontendCallback, "provider_error", providerErr, c.Query("error_description"))
@@ -859,6 +858,10 @@ func (h *AuthHandler) getWeChatOAuthConfig(ctx context.Context, rawMode string,
return wechatOAuthConfig{}, err
}
if h == nil || h.settingSvc == nil {
return wechatOAuthConfig{}, infraerrors.ServiceUnavailable("CONFIG_NOT_READY", "wechat oauth settings service not ready")
}
apiBaseURL := ""
if h != nil && h.settingSvc != nil {
settings, err := h.settingSvc.GetAllSettings(ctx)
@@ -867,27 +870,28 @@ func (h *AuthHandler) getWeChatOAuthConfig(ctx context.Context, rawMode string,
}
}
effective, err := h.settingSvc.GetWeChatConnectOAuthConfig(ctx)
if err != nil {
return wechatOAuthConfig{}, err
}
if effective.Mode != mode {
return wechatOAuthConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "wechat oauth is disabled")
}
cfg := wechatOAuthConfig{
mode: mode,
redirectURI: resolveWeChatOAuthAbsoluteURL(apiBaseURL, c, "/api/v1/auth/oauth/wechat/callback"),
frontendCallback: wechatOAuthFrontendCallback(),
appID: strings.TrimSpace(effective.AppID),
appSecret: strings.TrimSpace(effective.AppSecret),
redirectURI: firstNonEmpty(strings.TrimSpace(effective.RedirectURL), resolveWeChatOAuthAbsoluteURL(apiBaseURL, c, "/api/v1/auth/oauth/wechat/callback")),
frontendCallback: firstNonEmpty(strings.TrimSpace(effective.FrontendRedirectURL), wechatOAuthDefaultFrontendCB),
scope: firstNonEmpty(strings.TrimSpace(effective.Scopes), service.DefaultWeChatConnectScopesForMode(mode)),
}
switch mode {
case "mp":
cfg.appID = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_ID"))
cfg.appSecret = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_SECRET"))
cfg.authorizeURL = "https://open.weixin.qq.com/connect/oauth2/authorize"
cfg.scope = "snsapi_userinfo"
default:
cfg.appID = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_OPEN_APP_ID"))
cfg.appSecret = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_OPEN_APP_SECRET"))
cfg.authorizeURL = "https://open.weixin.qq.com/connect/qrconnect"
cfg.scope = "snsapi_login"
}
if cfg.appID == "" || cfg.appSecret == "" {
return wechatOAuthConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "wechat oauth is disabled")
}
if strings.TrimSpace(cfg.redirectURI) == "" {
return wechatOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth redirect url not configured")
@@ -896,8 +900,14 @@ func (h *AuthHandler) getWeChatOAuthConfig(ctx context.Context, rawMode string,
return cfg, nil
}
func wechatOAuthFrontendCallback() string {
return firstNonEmpty(strings.TrimSpace(os.Getenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL")), wechatOAuthDefaultFrontendCB)
func (h *AuthHandler) wechatOAuthFrontendCallback(ctx context.Context) string {
if h != nil && h.settingSvc != nil {
cfg, err := h.settingSvc.GetWeChatConnectOAuthConfig(ctx)
if err == nil && strings.TrimSpace(cfg.FrontendRedirectURL) != "" {
return strings.TrimSpace(cfg.FrontendRedirectURL)
}
}
return wechatOAuthDefaultFrontendCB
}
func resolveWeChatOAuthMode(rawMode string, c *gin.Context) (string, error) {

View File

@@ -33,16 +33,22 @@ import (
)
func TestWeChatOAuthStartRedirectsAndSetsPendingCookies(t *testing.T) {
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
gin.SetMode(gin.TestMode)
handler, client := newWeChatOAuthTestHandlerWithSettings(t, false, map[string]string{
service.SettingKeyWeChatConnectEnabled: "true",
service.SettingKeyWeChatConnectAppID: "wx-open-app",
service.SettingKeyWeChatConnectAppSecret: "wx-open-secret",
service.SettingKeyWeChatConnectMode: "open",
service.SettingKeyWeChatConnectScopes: "snsapi_login",
service.SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
service.SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
})
defer client.Close()
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/wechat/start?mode=open&redirect=/billing", nil)
c.Request.Host = "api.example.com"
handler := &AuthHandler{}
handler.WeChatOAuthStart(c)
require.Equal(t, http.StatusFound, recorder.Code)
@@ -60,10 +66,6 @@ func TestWeChatOAuthStartRedirectsAndSetsPendingCookies(t *testing.T) {
}
func TestWeChatOAuthCallbackCreatesPendingSessionForUnifiedFlow(t *testing.T) {
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "/auth/wechat/callback")
originalAccessTokenURL := wechatOAuthAccessTokenURL
originalUserInfoURL := wechatOAuthUserInfoURL
t.Cleanup(func() {
@@ -124,10 +126,6 @@ func TestWeChatOAuthCallbackCreatesPendingSessionForUnifiedFlow(t *testing.T) {
}
func TestWeChatOAuthCallbackRejectsMissingUnionID(t *testing.T) {
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "https://app.example.com/auth/wechat/callback")
originalAccessTokenURL := wechatOAuthAccessTokenURL
originalUserInfoURL := wechatOAuthUserInfoURL
t.Cleanup(func() {
@@ -151,7 +149,7 @@ func TestWeChatOAuthCallbackRejectsMissingUnionID(t *testing.T) {
wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
wechatOAuthUserInfoURL = upstream.URL + "/sns/userinfo"
handler, client := newWeChatOAuthTestHandler(t, false)
handler, client := newWeChatOAuthTestHandlerWithSettings(t, false, wechatOAuthTestSettings("open", "wx-open-app", "wx-open-secret", "https://app.example.com/auth/wechat/callback"))
defer client.Close()
recorder := httptest.NewRecorder()
@@ -177,9 +175,6 @@ func TestWeChatOAuthCallbackRejectsMissingUnionID(t *testing.T) {
}
func TestWeChatPaymentOAuthCallbackRedirectsWithOpaqueResumeToken(t *testing.T) {
t.Setenv("WECHAT_OAUTH_MP_APP_ID", "wx-mp-app")
t.Setenv("WECHAT_OAUTH_MP_APP_SECRET", "wx-mp-secret")
originalAccessTokenURL := wechatOAuthAccessTokenURL
t.Cleanup(func() {
wechatOAuthAccessTokenURL = originalAccessTokenURL
@@ -196,7 +191,7 @@ func TestWeChatPaymentOAuthCallbackRedirectsWithOpaqueResumeToken(t *testing.T)
defer upstream.Close()
wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
handler, client := newWeChatOAuthTestHandler(t, false)
handler, client := newWeChatOAuthTestHandlerWithSettings(t, false, wechatOAuthTestSettings("mp", "wx-mp-app", "wx-mp-secret", "/auth/wechat/callback"))
defer client.Close()
handler.cfg.Totp.EncryptionKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
@@ -240,7 +235,6 @@ func TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels(t *test
testCases := []struct {
name string
mode string
appIDEnv string
appID string
appSecret string
openID string
@@ -248,7 +242,6 @@ func TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels(t *test
{
name: "open",
mode: "open",
appIDEnv: "WECHAT_OAUTH_OPEN_APP_ID",
appID: "wx-open-app",
appSecret: "wx-open-secret",
openID: "openid-open-123",
@@ -256,7 +249,6 @@ func TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels(t *test
{
name: "mp",
mode: "mp",
appIDEnv: "WECHAT_OAUTH_MP_APP_ID",
appID: "wx-mp-app",
appSecret: "wx-mp-secret",
openID: "openid-mp-123",
@@ -265,15 +257,6 @@ func TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels(t *test
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Setenv(tc.appIDEnv, tc.appID)
switch tc.mode {
case "open":
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", tc.appSecret)
case "mp":
t.Setenv("WECHAT_OAUTH_MP_APP_SECRET", tc.appSecret)
}
t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "/auth/wechat/callback")
originalAccessTokenURL := wechatOAuthAccessTokenURL
originalUserInfoURL := wechatOAuthUserInfoURL
t.Cleanup(func() {
@@ -297,7 +280,7 @@ func TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels(t *test
wechatOAuthAccessTokenURL = upstream.URL + "/sns/oauth2/access_token"
wechatOAuthUserInfoURL = upstream.URL + "/sns/userinfo"
handler, client := newWeChatOAuthTestHandler(t, false)
handler, client := newWeChatOAuthTestHandlerWithSettings(t, false, wechatOAuthTestSettings(tc.mode, tc.appID, tc.appSecret, "/auth/wechat/callback"))
defer client.Close()
currentUser, err := client.User.Create().
@@ -354,10 +337,6 @@ func TestWeChatOAuthCallbackBindUsesUnionCanonicalIdentityAcrossChannels(t *test
}
func TestWeChatOAuthCallbackBindRejectsCanonicalOwnershipConflict(t *testing.T) {
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "/auth/wechat/callback")
originalAccessTokenURL := wechatOAuthAccessTokenURL
originalUserInfoURL := wechatOAuthUserInfoURL
t.Cleanup(func() {
@@ -436,10 +415,6 @@ func TestWeChatOAuthCallbackBindRejectsCanonicalOwnershipConflict(t *testing.T)
}
func TestWeChatOAuthCallbackBindRejectsChannelOwnershipConflict(t *testing.T) {
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "/auth/wechat/callback")
originalAccessTokenURL := wechatOAuthAccessTokenURL
originalUserInfoURL := wechatOAuthUserInfoURL
t.Cleanup(func() {
@@ -529,10 +504,6 @@ func TestWeChatOAuthCallbackBindRejectsChannelOwnershipConflict(t *testing.T) {
}
func TestWeChatOAuthCallbackBindRejectsLegacyProviderKeyOwnershipConflict(t *testing.T) {
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "/auth/wechat/callback")
originalAccessTokenURL := wechatOAuthAccessTokenURL
originalUserInfoURL := wechatOAuthUserInfoURL
t.Cleanup(func() {
@@ -611,10 +582,6 @@ func TestWeChatOAuthCallbackBindRejectsLegacyProviderKeyOwnershipConflict(t *tes
}
func TestCompleteWeChatOAuthRegistrationAfterInvitationPendingSession(t *testing.T) {
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "/auth/wechat/callback")
originalAccessTokenURL := wechatOAuthAccessTokenURL
originalUserInfoURL := wechatOAuthUserInfoURL
t.Cleanup(func() {
@@ -737,10 +704,6 @@ func TestCompleteWeChatOAuthRegistrationAfterInvitationPendingSession(t *testing
}
func TestWeChatOAuthCallbackRepairsLegacyOpenIDOnlyIdentity(t *testing.T) {
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "/auth/wechat/callback")
originalAccessTokenURL := wechatOAuthAccessTokenURL
originalUserInfoURL := wechatOAuthUserInfoURL
t.Cleanup(func() {
@@ -900,10 +863,6 @@ func TestCompleteWeChatOAuthRegistrationRejectsAdoptExistingUserSession(t *testi
}
func TestWeChatOAuthCallbackRepairsLegacyProviderKeyCanonicalIdentity(t *testing.T) {
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
t.Setenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL", "/auth/wechat/callback")
originalAccessTokenURL := wechatOAuthAccessTokenURL
originalUserInfoURL := wechatOAuthUserInfoURL
t.Cleanup(func() {
@@ -1010,6 +969,22 @@ func TestWeChatOAuthCallbackRepairsLegacyProviderKeyCanonicalIdentity(t *testing
}
func newWeChatOAuthTestHandler(t *testing.T, invitationEnabled bool) (*AuthHandler, *dbent.Client) {
return newWeChatOAuthTestHandlerWithSettings(t, invitationEnabled, nil)
}
func wechatOAuthTestSettings(mode, appID, secret, frontendRedirect string) map[string]string {
return map[string]string{
service.SettingKeyWeChatConnectEnabled: "true",
service.SettingKeyWeChatConnectAppID: appID,
service.SettingKeyWeChatConnectAppSecret: secret,
service.SettingKeyWeChatConnectMode: mode,
service.SettingKeyWeChatConnectScopes: service.DefaultWeChatConnectScopesForMode(mode),
service.SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
service.SettingKeyWeChatConnectFrontendRedirectURL: frontendRedirect,
}
}
func newWeChatOAuthTestHandlerWithSettings(t *testing.T, invitationEnabled bool, extraSettings map[string]string) (*AuthHandler, *dbent.Client) {
t.Helper()
db, err := sql.Open("sqlite", "file:auth_wechat_oauth?mode=memory&cache=shared")
@@ -1036,12 +1011,17 @@ func newWeChatOAuthTestHandler(t *testing.T, invitationEnabled bool) (*AuthHandl
UserConcurrency: 1,
},
}
settingSvc := service.NewSettingService(&wechatOAuthSettingRepoStub{
values: map[string]string{
service.SettingKeyRegistrationEnabled: "true",
service.SettingKeyInvitationCodeEnabled: boolSettingValue(invitationEnabled),
},
}, cfg)
values := map[string]string{
service.SettingKeyRegistrationEnabled: "true",
service.SettingKeyInvitationCodeEnabled: boolSettingValue(invitationEnabled),
}
for key, value := range wechatOAuthTestSettings("open", "wx-open-app", "wx-open-secret", "/auth/wechat/callback") {
values[key] = value
}
for key, value := range extraSettings {
values[key] = value
}
settingSvc := service.NewSettingService(&wechatOAuthSettingRepoStub{values: values}, cfg)
authSvc := service.NewAuthService(
client,

View File

@@ -51,6 +51,14 @@ type SystemSettings struct {
LinuxDoConnectClientSecretConfigured bool `json:"linuxdo_connect_client_secret_configured"`
LinuxDoConnectRedirectURL string `json:"linuxdo_connect_redirect_url"`
WeChatConnectEnabled bool `json:"wechat_connect_enabled"`
WeChatConnectAppID string `json:"wechat_connect_app_id"`
WeChatConnectAppSecretConfigured bool `json:"wechat_connect_app_secret_configured"`
WeChatConnectMode string `json:"wechat_connect_mode"`
WeChatConnectScopes string `json:"wechat_connect_scopes"`
WeChatConnectRedirectURL string `json:"wechat_connect_redirect_url"`
WeChatConnectFrontendRedirectURL string `json:"wechat_connect_frontend_redirect_url"`
OIDCConnectEnabled bool `json:"oidc_connect_enabled"`
OIDCConnectProviderName string `json:"oidc_connect_provider_name"`
OIDCConnectClientID string `json:"oidc_connect_client_id"`

View File

@@ -84,12 +84,17 @@ func TestSettingHandler_GetPublicSettings_ExposesForceEmailOnThirdPartySignup(t
func TestSettingHandler_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
t.Setenv("WECHAT_OAUTH_MP_APP_ID", "")
t.Setenv("WECHAT_OAUTH_MP_APP_SECRET", "")
h := NewSettingHandler(service.NewSettingService(&settingHandlerPublicRepoStub{}, &config.Config{}), "test-version")
h := NewSettingHandler(service.NewSettingService(&settingHandlerPublicRepoStub{
values: map[string]string{
service.SettingKeyWeChatConnectEnabled: "true",
service.SettingKeyWeChatConnectAppID: "wx-mp-app",
service.SettingKeyWeChatConnectAppSecret: "wx-mp-secret",
service.SettingKeyWeChatConnectMode: "mp",
service.SettingKeyWeChatConnectScopes: "snsapi_base",
service.SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
service.SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
},
}, &config.Config{}), "test-version")
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
@@ -110,6 +115,6 @@ func TestSettingHandler_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t *
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp))
require.Equal(t, 0, resp.Code)
require.True(t, resp.Data.WeChatOAuthEnabled)
require.True(t, resp.Data.WeChatOAuthOpenEnabled)
require.False(t, resp.Data.WeChatOAuthMPEnabled)
require.False(t, resp.Data.WeChatOAuthOpenEnabled)
require.True(t, resp.Data.WeChatOAuthMPEnabled)
}

View File

@@ -111,6 +111,15 @@ const (
SettingKeyLinuxDoConnectClientSecret = "linuxdo_connect_client_secret"
SettingKeyLinuxDoConnectRedirectURL = "linuxdo_connect_redirect_url"
// WeChat Connect OAuth 登录设置
SettingKeyWeChatConnectEnabled = "wechat_connect_enabled"
SettingKeyWeChatConnectAppID = "wechat_connect_app_id"
SettingKeyWeChatConnectAppSecret = "wechat_connect_app_secret"
SettingKeyWeChatConnectMode = "wechat_connect_mode"
SettingKeyWeChatConnectScopes = "wechat_connect_scopes"
SettingKeyWeChatConnectRedirectURL = "wechat_connect_redirect_url"
SettingKeyWeChatConnectFrontendRedirectURL = "wechat_connect_frontend_redirect_url"
// Generic OIDC OAuth 登录设置
SettingKeyOIDCConnectEnabled = "oidc_connect_enabled"
SettingKeyOIDCConnectProviderName = "oidc_connect_provider_name"

View File

@@ -93,6 +93,11 @@ type UpdatePaymentConfigRequest struct {
CancelRateLimitWindow *int `json:"cancel_rate_limit_window"`
CancelRateLimitUnit *string `json:"cancel_rate_limit_unit"`
CancelRateLimitMode *string `json:"cancel_rate_limit_window_mode"`
VisibleMethodAlipaySource *string `json:"payment_visible_method_alipay_source"`
VisibleMethodWxpaySource *string `json:"payment_visible_method_wxpay_source"`
VisibleMethodAlipayEnabled *bool `json:"payment_visible_method_alipay_enabled"`
VisibleMethodWxpayEnabled *bool `json:"payment_visible_method_wxpay_enabled"`
}
// MethodLimits holds per-payment-type limits.
@@ -319,6 +324,10 @@ func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req Upda
SettingCancelWindowSize: formatPositiveInt(req.CancelRateLimitWindow),
SettingCancelWindowUnit: derefStr(req.CancelRateLimitUnit),
SettingCancelWindowMode: derefStr(req.CancelRateLimitMode),
SettingPaymentVisibleMethodAlipaySource: derefStr(req.VisibleMethodAlipaySource),
SettingPaymentVisibleMethodWxpaySource: derefStr(req.VisibleMethodWxpaySource),
SettingPaymentVisibleMethodAlipayEnabled: formatBoolOrEmpty(req.VisibleMethodAlipayEnabled),
SettingPaymentVisibleMethodWxpayEnabled: formatBoolOrEmpty(req.VisibleMethodWxpayEnabled),
}
if req.EnabledTypes != nil {
m[SettingEnabledPaymentTypes] = strings.Join(req.EnabledTypes, ",")

View File

@@ -366,7 +366,8 @@ func newPaymentConfigServiceTestClient(t *testing.T) *dbent.Client {
}
type paymentConfigSettingRepoStub struct {
values map[string]string
values map[string]string
updates map[string]string
}
func (s *paymentConfigSettingRepoStub) Get(context.Context, string) (*Setting, error) {
@@ -383,10 +384,52 @@ func (s *paymentConfigSettingRepoStub) GetMultiple(_ context.Context, keys []str
}
return out, nil
}
func (s *paymentConfigSettingRepoStub) SetMultiple(context.Context, map[string]string) error {
func (s *paymentConfigSettingRepoStub) SetMultiple(_ context.Context, values map[string]string) error {
s.updates = make(map[string]string, len(values))
for key, value := range values {
s.updates[key] = value
if s.values == nil {
s.values = map[string]string{}
}
s.values[key] = value
}
return nil
}
func (s *paymentConfigSettingRepoStub) GetAll(context.Context) (map[string]string, error) {
return s.values, nil
}
func (s *paymentConfigSettingRepoStub) Delete(context.Context, string) error { return nil }
func TestUpdatePaymentConfig_PersistsVisibleMethodRouting(t *testing.T) {
repo := &paymentConfigSettingRepoStub{values: map[string]string{}}
svc := &PaymentConfigService{settingRepo: repo}
alipayEnabled := true
wxpayEnabled := false
err := svc.UpdatePaymentConfig(context.Background(), UpdatePaymentConfigRequest{
VisibleMethodAlipayEnabled: &alipayEnabled,
VisibleMethodAlipaySource: paymentConfigStrPtr(VisibleMethodSourceEasyPayAlipay),
VisibleMethodWxpayEnabled: &wxpayEnabled,
VisibleMethodWxpaySource: paymentConfigStrPtr(VisibleMethodSourceOfficialWechat),
})
if err != nil {
t.Fatalf("UpdatePaymentConfig returned error: %v", err)
}
if repo.values[SettingPaymentVisibleMethodAlipayEnabled] != "true" {
t.Fatalf("alipay enabled = %q, want true", repo.values[SettingPaymentVisibleMethodAlipayEnabled])
}
if repo.values[SettingPaymentVisibleMethodAlipaySource] != VisibleMethodSourceEasyPayAlipay {
t.Fatalf("alipay source = %q, want %q", repo.values[SettingPaymentVisibleMethodAlipaySource], VisibleMethodSourceEasyPayAlipay)
}
if repo.values[SettingPaymentVisibleMethodWxpayEnabled] != "false" {
t.Fatalf("wxpay enabled = %q, want false", repo.values[SettingPaymentVisibleMethodWxpayEnabled])
}
if repo.values[SettingPaymentVisibleMethodWxpaySource] != VisibleMethodSourceOfficialWechat {
t.Fatalf("wxpay source = %q, want %q", repo.values[SettingPaymentVisibleMethodWxpaySource], VisibleMethodSourceOfficialWechat)
}
}
func paymentConfigStrPtr(value string) *string {
return &value
}

View File

@@ -6,7 +6,6 @@ import (
"log/slog"
"math"
"net/url"
"os"
"strconv"
"strings"
"time"
@@ -512,16 +511,21 @@ func requiresWeChatJSAPICompatibleSelection(req CreateOrderRequest, sel *payment
return req.IsWeChatBrowser || strings.TrimSpace(req.OpenID) != ""
}
func (s *PaymentService) getWeChatPaymentOAuthCredential(context.Context) (string, string, error) {
appID := strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_ID"))
appSecret := strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_SECRET"))
if appID == "" || appSecret == "" {
func (s *PaymentService) getWeChatPaymentOAuthCredential(ctx context.Context) (string, string, error) {
if s == nil || s.configService == nil || s.configService.settingRepo == nil {
return "", "", infraerrors.ServiceUnavailable(
"WECHAT_PAYMENT_MP_NOT_CONFIGURED",
"wechat in-app payment requires a complete WeChat MP OAuth credential",
)
}
return appID, appSecret, nil
cfg, err := (&SettingService{settingRepo: s.configService.settingRepo}).GetWeChatConnectOAuthConfig(ctx)
if err != nil || cfg.Mode != "mp" || strings.TrimSpace(cfg.AppID) == "" || strings.TrimSpace(cfg.AppSecret) == "" {
return "", "", infraerrors.ServiceUnavailable(
"WECHAT_PAYMENT_MP_NOT_CONFIGURED",
"wechat in-app payment requires a complete WeChat MP OAuth credential",
)
}
return strings.TrimSpace(cfg.AppID), strings.TrimSpace(cfg.AppSecret), nil
}
func classifyCreatePaymentError(req CreateOrderRequest, providerKey string, err error) error {

View File

@@ -60,10 +60,17 @@ func TestSelectCreateOrderInstancePrefersJSAPICompatibleWxpayInstance(t *testing
}
configService := &PaymentConfigService{
entClient: client,
entClient: client,
settingRepo: &paymentConfigSettingRepoStub{values: map[string]string{
SettingPaymentVisibleMethodWxpayEnabled: "true",
SettingPaymentVisibleMethodWxpaySource: VisibleMethodSourceOfficialWechat,
SettingPaymentVisibleMethodWxpayEnabled: "true",
SettingPaymentVisibleMethodWxpaySource: VisibleMethodSourceOfficialWechat,
SettingKeyWeChatConnectEnabled: "true",
SettingKeyWeChatConnectAppID: "wx-mp-app",
SettingKeyWeChatConnectAppSecret: "wechat-secret",
SettingKeyWeChatConnectMode: "mp",
SettingKeyWeChatConnectScopes: "snsapi_base",
SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
}},
encryptionKey: []byte(jsapiTestEncryptionKey),
}
@@ -77,9 +84,6 @@ func TestSelectCreateOrderInstancePrefersJSAPICompatibleWxpayInstance(t *testing
configService: configService,
}
t.Setenv("WECHAT_OAUTH_MP_APP_ID", "wx-mp-app")
t.Setenv("WECHAT_OAUTH_MP_APP_SECRET", "wechat-secret")
sel, err := svc.selectCreateOrderInstance(ctx, CreateOrderRequest{
PaymentType: payment.TypeWxpay,
OpenID: "openid-123",

View File

@@ -91,10 +91,15 @@ func TestBuildCreateOrderResponseCopiesJSAPIPayload(t *testing.T) {
}
func TestMaybeBuildWeChatOAuthRequiredResponse(t *testing.T) {
t.Setenv("WECHAT_OAUTH_MP_APP_ID", "wx123456")
t.Setenv("WECHAT_OAUTH_MP_APP_SECRET", "wechat-secret")
svc := &PaymentService{}
svc := newWeChatPaymentOAuthTestService(map[string]string{
SettingKeyWeChatConnectEnabled: "true",
SettingKeyWeChatConnectAppID: "wx123456",
SettingKeyWeChatConnectAppSecret: "wechat-secret",
SettingKeyWeChatConnectMode: "mp",
SettingKeyWeChatConnectScopes: "snsapi_base",
SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
})
resp, err := svc.maybeBuildWeChatOAuthRequiredResponse(context.Background(), CreateOrderRequest{
Amount: 12.5,
@@ -132,7 +137,7 @@ func TestMaybeBuildWeChatOAuthRequiredResponse(t *testing.T) {
func TestMaybeBuildWeChatOAuthRequiredResponseRequiresMPConfigInWeChat(t *testing.T) {
t.Parallel()
svc := &PaymentService{}
svc := newWeChatPaymentOAuthTestService(nil)
resp, err := svc.maybeBuildWeChatOAuthRequiredResponse(context.Background(), CreateOrderRequest{
Amount: 12.5,
@@ -155,10 +160,15 @@ func TestMaybeBuildWeChatOAuthRequiredResponseRequiresMPConfigInWeChat(t *testin
}
func TestMaybeBuildWeChatOAuthRequiredResponseForSelectionSkipsEasyPayProvider(t *testing.T) {
t.Setenv("WECHAT_OAUTH_MP_APP_ID", "wx123456")
t.Setenv("WECHAT_OAUTH_MP_APP_SECRET", "wechat-secret")
svc := &PaymentService{}
svc := newWeChatPaymentOAuthTestService(map[string]string{
SettingKeyWeChatConnectEnabled: "true",
SettingKeyWeChatConnectAppID: "wx123456",
SettingKeyWeChatConnectAppSecret: "wechat-secret",
SettingKeyWeChatConnectMode: "mp",
SettingKeyWeChatConnectScopes: "snsapi_base",
SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
})
resp, err := svc.maybeBuildWeChatOAuthRequiredResponseForSelection(context.Background(), CreateOrderRequest{
Amount: 12.5,
@@ -175,3 +185,11 @@ func TestMaybeBuildWeChatOAuthRequiredResponseForSelectionSkipsEasyPayProvider(t
t.Fatalf("expected nil response, got %+v", resp)
}
}
func newWeChatPaymentOAuthTestService(values map[string]string) *PaymentService {
return &PaymentService{
configService: &PaymentConfigService{
settingRepo: &paymentConfigSettingRepoStub{values: values},
},
}
}

View File

@@ -9,7 +9,6 @@ import (
"fmt"
"log/slog"
"net/url"
"os"
"sort"
"strconv"
"strings"
@@ -173,8 +172,43 @@ var (
const (
defaultAuthSourceBalance = 0
defaultAuthSourceConcurrency = 5
defaultWeChatConnectMode = "open"
defaultWeChatConnectScopes = "snsapi_login"
defaultWeChatConnectFrontend = "/auth/wechat/callback"
)
func normalizeWeChatConnectModeSetting(raw string) string {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "mp":
return "mp"
default:
return "open"
}
}
func defaultWeChatConnectScopeForMode(mode string) string {
if normalizeWeChatConnectModeSetting(mode) == "mp" {
return "snsapi_userinfo"
}
return defaultWeChatConnectScopes
}
func normalizeWeChatConnectScopeSetting(raw, mode string) string {
switch normalizeWeChatConnectModeSetting(mode) {
case "mp":
switch strings.TrimSpace(raw) {
case "snsapi_base":
return "snsapi_base"
case "snsapi_userinfo":
return "snsapi_userinfo"
default:
return defaultWeChatConnectScopeForMode(mode)
}
default:
return defaultWeChatConnectScopes
}
}
// NewSettingService 创建系统设置服务实例
func NewSettingService(settingRepo SettingRepository, cfg *config.Config) *SettingService {
return &SettingService{
@@ -240,6 +274,13 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
SettingKeyCustomMenuItems,
SettingKeyCustomEndpoints,
SettingKeyLinuxDoConnectEnabled,
SettingKeyWeChatConnectEnabled,
SettingKeyWeChatConnectAppID,
SettingKeyWeChatConnectAppSecret,
SettingKeyWeChatConnectMode,
SettingKeyWeChatConnectScopes,
SettingKeyWeChatConnectRedirectURL,
SettingKeyWeChatConnectFrontendRedirectURL,
SettingKeyBackendModeEnabled,
SettingPaymentEnabled,
SettingKeyOIDCConnectEnabled,
@@ -274,9 +315,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
if oidcProviderName == "" {
oidcProviderName = "OIDC"
}
weChatOpenEnabled := isWeChatOAuthOpenConfigured()
weChatMPEnabled := isWeChatOAuthMPConfigured()
weChatEnabled := weChatOpenEnabled || weChatMPEnabled
weChatEnabled, weChatOpenEnabled, weChatMPEnabled := s.weChatOAuthCapabilitiesFromSettings(settings)
// Password reset requires email verification to be enabled
emailVerifyEnabled := settings[SettingKeyEmailVerifyEnabled] == "true"
@@ -431,6 +470,56 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
}, nil
}
func DefaultWeChatConnectScopesForMode(mode string) string {
return defaultWeChatConnectScopeForMode(mode)
}
func (s *SettingService) parseWeChatConnectOAuthConfig(settings map[string]string) (WeChatConnectOAuthConfig, error) {
cfg := WeChatConnectOAuthConfig{
Enabled: settings[SettingKeyWeChatConnectEnabled] == "true",
AppID: strings.TrimSpace(settings[SettingKeyWeChatConnectAppID]),
AppSecret: strings.TrimSpace(settings[SettingKeyWeChatConnectAppSecret]),
Mode: normalizeWeChatConnectModeSetting(settings[SettingKeyWeChatConnectMode]),
Scopes: normalizeWeChatConnectScopeSetting(settings[SettingKeyWeChatConnectScopes], settings[SettingKeyWeChatConnectMode]),
RedirectURL: strings.TrimSpace(settings[SettingKeyWeChatConnectRedirectURL]),
FrontendRedirectURL: strings.TrimSpace(settings[SettingKeyWeChatConnectFrontendRedirectURL]),
}
if cfg.FrontendRedirectURL == "" {
cfg.FrontendRedirectURL = defaultWeChatConnectFrontend
}
if !cfg.Enabled {
return WeChatConnectOAuthConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "wechat oauth is disabled")
}
if cfg.AppID == "" {
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth app id not configured")
}
if cfg.AppSecret == "" {
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth app secret not configured")
}
if cfg.RedirectURL == "" {
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth redirect url not configured")
}
if cfg.FrontendRedirectURL == "" {
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth frontend redirect url not configured")
}
if err := config.ValidateAbsoluteHTTPURL(cfg.RedirectURL); err != nil {
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth redirect url invalid")
}
if err := config.ValidateFrontendRedirectURL(cfg.FrontendRedirectURL); err != nil {
return WeChatConnectOAuthConfig{}, infraerrors.InternalServer("OAUTH_CONFIG_INVALID", "wechat oauth frontend redirect url invalid")
}
return cfg, nil
}
func (s *SettingService) weChatOAuthCapabilitiesFromSettings(settings map[string]string) (bool, bool, bool) {
cfg, err := s.parseWeChatConnectOAuthConfig(settings)
if err != nil {
return false, false, false
}
return true, cfg.Mode == "open", cfg.Mode == "mp"
}
// filterUserVisibleMenuItems filters out admin-only menu items from a raw JSON
// array string, returning only items with visibility != "admin".
func filterUserVisibleMenuItems(raw string) json.RawMessage {
@@ -467,20 +556,6 @@ func filterUserVisibleMenuItems(raw string) json.RawMessage {
return result
}
func isWeChatOAuthConfigured() bool {
return isWeChatOAuthOpenConfigured() || isWeChatOAuthMPConfigured()
}
func isWeChatOAuthOpenConfigured() bool {
return strings.TrimSpace(os.Getenv("WECHAT_OAUTH_OPEN_APP_ID")) != "" &&
strings.TrimSpace(os.Getenv("WECHAT_OAUTH_OPEN_APP_SECRET")) != ""
}
func isWeChatOAuthMPConfigured() bool {
return strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_ID")) != "" &&
strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_SECRET")) != ""
}
// safeRawJSONArray returns raw as json.RawMessage if it's valid JSON, otherwise "[]".
func safeRawJSONArray(raw string) json.RawMessage {
raw = strings.TrimSpace(raw)
@@ -625,6 +700,15 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
}
settings.PaymentVisibleMethodAlipaySource = alipaySource
settings.PaymentVisibleMethodWxpaySource = wxpaySource
settings.WeChatConnectAppID = strings.TrimSpace(settings.WeChatConnectAppID)
settings.WeChatConnectAppSecret = strings.TrimSpace(settings.WeChatConnectAppSecret)
settings.WeChatConnectMode = normalizeWeChatConnectModeSetting(settings.WeChatConnectMode)
settings.WeChatConnectScopes = normalizeWeChatConnectScopeSetting(settings.WeChatConnectScopes, settings.WeChatConnectMode)
settings.WeChatConnectRedirectURL = strings.TrimSpace(settings.WeChatConnectRedirectURL)
settings.WeChatConnectFrontendRedirectURL = strings.TrimSpace(settings.WeChatConnectFrontendRedirectURL)
if settings.WeChatConnectFrontendRedirectURL == "" {
settings.WeChatConnectFrontendRedirectURL = defaultWeChatConnectFrontend
}
updates := make(map[string]string)
@@ -694,6 +778,17 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
updates[SettingKeyOIDCConnectClientSecret] = settings.OIDCConnectClientSecret
}
// WeChat Connect OAuth 登录
updates[SettingKeyWeChatConnectEnabled] = strconv.FormatBool(settings.WeChatConnectEnabled)
updates[SettingKeyWeChatConnectAppID] = settings.WeChatConnectAppID
updates[SettingKeyWeChatConnectMode] = settings.WeChatConnectMode
updates[SettingKeyWeChatConnectScopes] = settings.WeChatConnectScopes
updates[SettingKeyWeChatConnectRedirectURL] = settings.WeChatConnectRedirectURL
updates[SettingKeyWeChatConnectFrontendRedirectURL] = settings.WeChatConnectFrontendRedirectURL
if settings.WeChatConnectAppSecret != "" {
updates[SettingKeyWeChatConnectAppSecret] = settings.WeChatConnectAppSecret
}
// OEM设置
updates[SettingKeySiteName] = settings.SiteName
updates[SettingKeySiteLogo] = settings.SiteLogo
@@ -1200,6 +1295,10 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
SettingKeyTablePageSizeOptions: "[10,20,50,100]",
SettingKeyCustomMenuItems: "[]",
SettingKeyCustomEndpoints: "[]",
SettingKeyWeChatConnectEnabled: "false",
SettingKeyWeChatConnectMode: "open",
SettingKeyWeChatConnectScopes: "snsapi_login",
SettingKeyWeChatConnectFrontendRedirectURL: defaultWeChatConnectFrontend,
SettingKeyOIDCConnectEnabled: "false",
SettingKeyOIDCConnectProviderName: "OIDC",
SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency),
@@ -1491,6 +1590,19 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
}
result.OIDCConnectClientSecretConfigured = result.OIDCConnectClientSecret != ""
// WeChat Connect 设置:完全以 DB 系统设置为准。
result.WeChatConnectEnabled = settings[SettingKeyWeChatConnectEnabled] == "true"
result.WeChatConnectAppID = strings.TrimSpace(settings[SettingKeyWeChatConnectAppID])
result.WeChatConnectAppSecret = strings.TrimSpace(settings[SettingKeyWeChatConnectAppSecret])
result.WeChatConnectAppSecretConfigured = result.WeChatConnectAppSecret != ""
result.WeChatConnectMode = normalizeWeChatConnectModeSetting(settings[SettingKeyWeChatConnectMode])
result.WeChatConnectScopes = normalizeWeChatConnectScopeSetting(settings[SettingKeyWeChatConnectScopes], settings[SettingKeyWeChatConnectMode])
result.WeChatConnectRedirectURL = strings.TrimSpace(settings[SettingKeyWeChatConnectRedirectURL])
result.WeChatConnectFrontendRedirectURL = strings.TrimSpace(settings[SettingKeyWeChatConnectFrontendRedirectURL])
if result.WeChatConnectFrontendRedirectURL == "" {
result.WeChatConnectFrontendRedirectURL = defaultWeChatConnectFrontend
}
// Model fallback settings
result.EnableModelFallback = settings[SettingKeyEnableModelFallback] == "true"
result.FallbackModelAnthropic = s.getStringOrDefault(settings, SettingKeyFallbackModelAnthropic, "claude-3-5-sonnet-20241022")
@@ -1972,6 +2084,26 @@ func (s *SettingService) GetLinuxDoConnectOAuthConfig(ctx context.Context) (conf
return effective, nil
}
// GetWeChatConnectOAuthConfig 返回用于登录的最终生效 WeChat Connect 配置。
//
// WeChat Connect 已回归 DB 系统设置模型,不再回退到 config/env。
func (s *SettingService) GetWeChatConnectOAuthConfig(ctx context.Context) (WeChatConnectOAuthConfig, error) {
keys := []string{
SettingKeyWeChatConnectEnabled,
SettingKeyWeChatConnectAppID,
SettingKeyWeChatConnectAppSecret,
SettingKeyWeChatConnectMode,
SettingKeyWeChatConnectScopes,
SettingKeyWeChatConnectRedirectURL,
SettingKeyWeChatConnectFrontendRedirectURL,
}
settings, err := s.settingRepo.GetMultiple(ctx, keys)
if err != nil {
return WeChatConnectOAuthConfig{}, fmt.Errorf("get wechat connect settings: %w", err)
}
return s.parseWeChatConnectOAuthConfig(settings)
}
// GetOverloadCooldownSettings 获取529过载冷却配置
func (s *SettingService) GetOverloadCooldownSettings(ctx context.Context) (*OverloadCooldownSettings, error) {
value, err := s.settingRepo.GetValue(ctx, SettingKeyOverloadCooldownSettings)

View File

@@ -92,16 +92,21 @@ func TestSettingService_GetPublicSettings_ExposesForceEmailOnThirdPartySignup(t
}
func TestSettingService_GetPublicSettings_ExposesWeChatOAuthModeCapabilities(t *testing.T) {
t.Setenv("WECHAT_OAUTH_OPEN_APP_ID", "wx-open-app")
t.Setenv("WECHAT_OAUTH_OPEN_APP_SECRET", "wx-open-secret")
t.Setenv("WECHAT_OAUTH_MP_APP_ID", "")
t.Setenv("WECHAT_OAUTH_MP_APP_SECRET", "")
svc := NewSettingService(&settingPublicRepoStub{}, &config.Config{})
svc := NewSettingService(&settingPublicRepoStub{
values: map[string]string{
SettingKeyWeChatConnectEnabled: "true",
SettingKeyWeChatConnectAppID: "wx-mp-app",
SettingKeyWeChatConnectAppSecret: "wx-mp-secret",
SettingKeyWeChatConnectMode: "mp",
SettingKeyWeChatConnectScopes: "snsapi_base",
SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
},
}, &config.Config{})
settings, err := svc.GetPublicSettings(context.Background())
require.NoError(t, err)
require.True(t, settings.WeChatOAuthEnabled)
require.True(t, settings.WeChatOAuthOpenEnabled)
require.False(t, settings.WeChatOAuthMPEnabled)
require.False(t, settings.WeChatOAuthOpenEnabled)
require.True(t, settings.WeChatOAuthMPEnabled)
}

View File

@@ -0,0 +1,77 @@
//go:build unit
package service
import (
"context"
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/stretchr/testify/require"
)
type settingWeChatRepoStub struct {
values map[string]string
}
func (s *settingWeChatRepoStub) Get(context.Context, string) (*Setting, error) {
panic("unexpected Get call")
}
func (s *settingWeChatRepoStub) GetValue(_ context.Context, key string) (string, error) {
if value, ok := s.values[key]; ok {
return value, nil
}
return "", ErrSettingNotFound
}
func (s *settingWeChatRepoStub) Set(context.Context, string, string) error {
panic("unexpected Set call")
}
func (s *settingWeChatRepoStub) GetMultiple(_ context.Context, keys []string) (map[string]string, error) {
out := make(map[string]string, len(keys))
for _, key := range keys {
if value, ok := s.values[key]; ok {
out[key] = value
}
}
return out, nil
}
func (s *settingWeChatRepoStub) SetMultiple(context.Context, map[string]string) error {
panic("unexpected SetMultiple call")
}
func (s *settingWeChatRepoStub) GetAll(context.Context) (map[string]string, error) {
panic("unexpected GetAll call")
}
func (s *settingWeChatRepoStub) Delete(context.Context, string) error {
panic("unexpected Delete call")
}
func TestSettingService_GetWeChatConnectOAuthConfig_UsesDatabaseOverrides(t *testing.T) {
repo := &settingWeChatRepoStub{
values: map[string]string{
SettingKeyWeChatConnectEnabled: "true",
SettingKeyWeChatConnectAppID: "wx-db-app",
SettingKeyWeChatConnectAppSecret: "wx-db-secret",
SettingKeyWeChatConnectMode: "mp",
SettingKeyWeChatConnectScopes: "snsapi_base",
SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
},
}
svc := NewSettingService(repo, &config.Config{})
got, err := svc.GetWeChatConnectOAuthConfig(context.Background())
require.NoError(t, err)
require.True(t, got.Enabled)
require.Equal(t, "wx-db-app", got.AppID)
require.Equal(t, "wx-db-secret", got.AppSecret)
require.Equal(t, "mp", got.Mode)
require.Equal(t, "snsapi_base", got.Scopes)
require.Equal(t, "https://api.example.com/api/v1/auth/oauth/wechat/callback", got.RedirectURL)
require.Equal(t, "/auth/wechat/callback", got.FrontendRedirectURL)
}

View File

@@ -31,6 +31,16 @@ type SystemSettings struct {
LinuxDoConnectClientSecretConfigured bool
LinuxDoConnectRedirectURL string
// WeChat Connect OAuth 登录
WeChatConnectEnabled bool
WeChatConnectAppID string
WeChatConnectAppSecret string
WeChatConnectAppSecretConfigured bool
WeChatConnectMode string
WeChatConnectScopes string
WeChatConnectRedirectURL string
WeChatConnectFrontendRedirectURL string
// Generic OIDC OAuth 登录
OIDCConnectEnabled bool
OIDCConnectProviderName string
@@ -177,6 +187,16 @@ type PublicSettings struct {
BalanceLowNotifyRechargeURL string
}
type WeChatConnectOAuthConfig struct {
Enabled bool
AppID string
AppSecret string
Mode string
Scopes string
RedirectURL string
FrontendRedirectURL string
}
// StreamTimeoutSettings 流超时处理配置(仅控制超时后的处理方式,超时判定由网关配置控制)
type StreamTimeoutSettings struct {
// Enabled 是否启用流超时处理