mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-27 11:14:59 +00:00
feat(auth): add local registration toggle and OAuth failure logging
Deployments that only allow third-party sign-up can now disable local registration end to end: the register endpoint returns 403 when skillhub.auth.local.registration-enabled is false, and the web UI hides the entry when SKILLHUB_WEB_REGISTRATION_ENABLED is false. Also log OAuth2 login failures so callback errors are diagnosable from server logs. Fixes the runtime-config entrypoint so registration defaults are actually exported to envsubst. Signed-off-by: yhd <yhd4711499@live.com>
This commit is contained in:
parent
94baeb5544
commit
1049a267fb
10 changed files with 53 additions and 15 deletions
|
|
@ -13,6 +13,7 @@ import com.iflytek.skillhub.dto.LocalLoginRequest;
|
|||
import com.iflytek.skillhub.dto.LocalRegisterRequest;
|
||||
import com.iflytek.skillhub.dto.PasswordResetConfirmRequest;
|
||||
import com.iflytek.skillhub.dto.PasswordResetRequestDto;
|
||||
import com.iflytek.skillhub.exception.ForbiddenException;
|
||||
import com.iflytek.skillhub.exception.UnauthorizedException;
|
||||
import com.iflytek.skillhub.metrics.SkillHubMetrics;
|
||||
import com.iflytek.skillhub.ratelimit.RateLimit;
|
||||
|
|
@ -20,6 +21,7 @@ import com.iflytek.skillhub.security.AuthFailureThrottleService;
|
|||
import com.iflytek.skillhub.service.AuthMeResponseAssembler;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
|
|
@ -40,6 +42,7 @@ public class LocalAuthController extends BaseApiController {
|
|||
private final AuthFailureThrottleService authFailureThrottleService;
|
||||
private final PasswordResetService passwordResetService;
|
||||
private final AuthMeResponseAssembler authMeResponseAssembler;
|
||||
private final boolean registrationEnabled;
|
||||
|
||||
public LocalAuthController(ApiResponseFactory responseFactory,
|
||||
LocalAuthService localAuthService,
|
||||
|
|
@ -47,7 +50,8 @@ public class LocalAuthController extends BaseApiController {
|
|||
PlatformSessionService platformSessionService,
|
||||
AuthFailureThrottleService authFailureThrottleService,
|
||||
PasswordResetService passwordResetService,
|
||||
AuthMeResponseAssembler authMeResponseAssembler) {
|
||||
AuthMeResponseAssembler authMeResponseAssembler,
|
||||
@Value("${skillhub.auth.local.registration-enabled:true}") boolean registrationEnabled) {
|
||||
super(responseFactory);
|
||||
this.localAuthService = localAuthService;
|
||||
this.skillHubMetrics = skillHubMetrics;
|
||||
|
|
@ -55,12 +59,16 @@ public class LocalAuthController extends BaseApiController {
|
|||
this.authFailureThrottleService = authFailureThrottleService;
|
||||
this.passwordResetService = passwordResetService;
|
||||
this.authMeResponseAssembler = authMeResponseAssembler;
|
||||
this.registrationEnabled = registrationEnabled;
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
@RateLimit(category = "auth-register", authenticated = 10, anonymous = 5, windowSeconds = 300)
|
||||
public ApiResponse<AuthMeResponse> register(@Valid @RequestBody LocalRegisterRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
if (!registrationEnabled) {
|
||||
throw new ForbiddenException("error.auth.local.registration.disabled");
|
||||
}
|
||||
PlatformPrincipal principal = localAuthService.register(request.username(), request.password(), request.email());
|
||||
skillHubMetrics.incrementUserRegister();
|
||||
platformSessionService.establishSession(principal, httpRequest);
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ error.auth.local.accountDisabled=This account has been disabled
|
|||
error.auth.local.accountPending=This account is pending activation
|
||||
error.auth.local.accountMerged=This account has been merged and can no longer be used to log in
|
||||
error.auth.local.locked=Too many failed attempts. Please try again in {0} minute(s)
|
||||
error.auth.local.registration.disabled=Local registration is disabled. Please sign in with an authorized third-party account.
|
||||
error.auth.login.throttled=Too many login attempts. Please try again in {0} minute(s)
|
||||
error.auth.direct.disabled=Direct authentication compatibility is disabled
|
||||
error.auth.direct.providerUnsupported=Unsupported direct authentication provider: {0}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ error.auth.local.accountDisabled=该账号已被禁用
|
|||
error.auth.local.accountPending=该账号尚未激活
|
||||
error.auth.local.accountMerged=该账号已合并,不能再用于登录
|
||||
error.auth.local.locked=连续失败次数过多,请在 {0} 分钟后重试
|
||||
error.auth.local.registration.disabled=本地注册已关闭,请使用授权的第三方账号登录
|
||||
error.auth.login.throttled=登录尝试过于频繁,请在 {0} 分钟后重试
|
||||
error.auth.direct.disabled=直连认证兼容层未启用
|
||||
error.auth.direct.providerUnsupported=不支持的直连认证提供方:{0}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package com.iflytek.skillhub.auth.oauth;
|
|||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
|
@ -16,6 +18,8 @@ import java.io.IOException;
|
|||
@Component
|
||||
public class OAuth2LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(OAuth2LoginFailureHandler.class);
|
||||
|
||||
private final OAuthLoginFlowService oauthLoginFlowService;
|
||||
|
||||
public OAuth2LoginFailureHandler(OAuthLoginFlowService oauthLoginFlowService) {
|
||||
|
|
@ -26,6 +30,7 @@ public class OAuth2LoginFailureHandler extends SimpleUrlAuthenticationFailureHan
|
|||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException exception)
|
||||
throws IOException, ServletException {
|
||||
log.error("OAuth2 login failed [uri={}]", request.getRequestURI(), exception);
|
||||
String returnTo = oauthLoginFlowService.consumeReturnTo(request.getSession(false));
|
||||
String redirectTarget = oauthLoginFlowService.resolveFailureRedirect(exception, returnTo);
|
||||
if (redirectTarget != null) {
|
||||
|
|
|
|||
|
|
@ -15,9 +15,13 @@ set -eu
|
|||
: "${SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_ENABLED:=false}"
|
||||
: "${SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_PROVIDER:=}"
|
||||
: "${SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_AUTO:=false}"
|
||||
# NB: `${VAR:=default}` only sets a shell variable, not an exported one, so
|
||||
# envsubst would still substitute an empty string. Assign and export explicitly.
|
||||
SKILLHUB_WEB_REGISTRATION_ENABLED="${SKILLHUB_WEB_REGISTRATION_ENABLED:-true}"
|
||||
export SKILLHUB_WEB_REGISTRATION_ENABLED
|
||||
|
||||
# Generate runtime-config.js
|
||||
envsubst '${SKILLHUB_WEB_API_BASE_URL} ${SKILLHUB_PUBLIC_BASE_URL} ${SKILLHUB_WEB_AUTH_DIRECT_ENABLED} ${SKILLHUB_WEB_AUTH_DIRECT_PROVIDER} ${SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_ENABLED} ${SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_PROVIDER} ${SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_AUTO}' \
|
||||
envsubst '${SKILLHUB_WEB_API_BASE_URL} ${SKILLHUB_PUBLIC_BASE_URL} ${SKILLHUB_WEB_AUTH_DIRECT_ENABLED} ${SKILLHUB_WEB_AUTH_DIRECT_PROVIDER} ${SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_ENABLED} ${SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_PROVIDER} ${SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_AUTO} ${SKILLHUB_WEB_REGISTRATION_ENABLED}' \
|
||||
< /usr/share/nginx/html/runtime-config.js.template \
|
||||
> /usr/share/nginx/html/runtime-config.js
|
||||
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@ window.__SKILLHUB_RUNTIME_CONFIG__ = {
|
|||
authDirectProvider: "${SKILLHUB_WEB_AUTH_DIRECT_PROVIDER}",
|
||||
authSessionBootstrapEnabled: "${SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_ENABLED}",
|
||||
authSessionBootstrapProvider: "${SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_PROVIDER}",
|
||||
authSessionBootstrapAuto: "${SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_AUTO}"
|
||||
authSessionBootstrapAuto: "${SKILLHUB_WEB_AUTH_SESSION_BOOTSTRAP_AUTO}",
|
||||
registrationEnabled: "${SKILLHUB_WEB_REGISTRATION_ENABLED}"
|
||||
};
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ type RuntimeConfig = {
|
|||
authSessionBootstrapEnabled?: string
|
||||
authSessionBootstrapProvider?: string
|
||||
authSessionBootstrapAuto?: string
|
||||
registrationEnabled?: string
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
@ -182,6 +183,14 @@ export function getSessionBootstrapRuntimeConfig(): SessionBootstrapRuntimeConfi
|
|||
}
|
||||
}
|
||||
|
||||
export function isLocalRegistrationEnabled(): boolean {
|
||||
const value = getRuntimeConfig().registrationEnabled
|
||||
if (value === undefined || value.trim() === '') {
|
||||
return true
|
||||
}
|
||||
return parseBooleanFlag(value)
|
||||
}
|
||||
|
||||
type ApiEnvelope<T> = {
|
||||
code: number
|
||||
msg: string
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ vi.mock('lucide-react', () => ({
|
|||
|
||||
vi.mock('@/api/client', () => ({
|
||||
getDirectAuthRuntimeConfig: () => ({ enabled: false }),
|
||||
isLocalRegistrationEnabled: () => true,
|
||||
}))
|
||||
|
||||
vi.mock('@/features/auth/login-button', () => ({
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Link, useNavigate, useSearch } from '@tanstack/react-router'
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { getDirectAuthRuntimeConfig } from '@/api/client'
|
||||
import { getDirectAuthRuntimeConfig, isLocalRegistrationEnabled } from '@/api/client'
|
||||
import { LoginButton } from '@/features/auth/login-button'
|
||||
import { SessionBootstrapEntry } from '@/features/auth/session-bootstrap-entry'
|
||||
import { useAuthMethods } from '@/features/auth/use-auth-methods'
|
||||
|
|
@ -166,15 +166,19 @@ export function LoginPage() {
|
|||
</Link>
|
||||
</p>
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
{t('login.noAccount')}
|
||||
{' '}
|
||||
<Link
|
||||
to="/register"
|
||||
search={{ returnTo }}
|
||||
className="font-medium text-primary hover:underline"
|
||||
>
|
||||
{t('login.register')}
|
||||
</Link>
|
||||
{isLocalRegistrationEnabled() ? (
|
||||
<>
|
||||
{t('login.noAccount')}
|
||||
{' '}
|
||||
<Link
|
||||
to="/register"
|
||||
search={{ returnTo }}
|
||||
className="font-medium text-primary hover:underline"
|
||||
>
|
||||
{t('login.register')}
|
||||
</Link>
|
||||
</>
|
||||
) : null}
|
||||
</p>
|
||||
</form>
|
||||
</TabsContent>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Link, useNavigate, useSearch } from '@tanstack/react-router'
|
||||
import { Link, Navigate, useNavigate, useSearch } from '@tanstack/react-router'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ApiError } from '@/api/client'
|
||||
import { ApiError, isLocalRegistrationEnabled } from '@/api/client'
|
||||
import { LoginButton } from '@/features/auth/login-button'
|
||||
import { useLocalRegister } from '@/features/auth/use-local-auth'
|
||||
import { Button } from '@/shared/ui/button'
|
||||
|
|
@ -63,6 +63,10 @@ export function RegisterPage() {
|
|||
|
||||
const returnTo = search.returnTo && search.returnTo.startsWith('/') ? search.returnTo : '/dashboard'
|
||||
|
||||
if (!isLocalRegistrationEnabled()) {
|
||||
return <Navigate to="/login" search={{ returnTo }} />
|
||||
}
|
||||
|
||||
function validateUsername(value: string) {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue