mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 15:26:36 +08:00
[优化] POW 系统接口方案
This commit is contained in:
@@ -1,23 +1,21 @@
|
||||
import { apiRequest } from '@/lib/api/client';
|
||||
import {
|
||||
clearStoredOpenFlareToken,
|
||||
setStoredOpenFlareToken,
|
||||
} from '@/lib/api/auth-token';
|
||||
import type {
|
||||
AuthUser,
|
||||
LoginPayload,
|
||||
PasswordResetRequestPayload,
|
||||
RegisterPayload,
|
||||
} from '@/types/auth';
|
||||
import {apiRequest} from '@/lib/api/client';
|
||||
import {clearStoredOpenFlareToken, setStoredOpenFlareToken,} from '@/lib/api/auth-token';
|
||||
import type {AuthUser, LoginPayload, PasswordResetRequestPayload, RegisterPayload,} from '@/types/auth';
|
||||
|
||||
export function getCurrentUser() {
|
||||
return apiRequest<AuthUser>('/user/self');
|
||||
}
|
||||
|
||||
export function login(payload: LoginPayload) {
|
||||
const { cap_token, ...body } = payload;
|
||||
const headers: Record<string, string> = {};
|
||||
if (cap_token) {
|
||||
headers['X-Cap-Token'] = cap_token;
|
||||
}
|
||||
return apiRequest<AuthUser>('/user/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
}).then((user) => {
|
||||
if (user.token) {
|
||||
setStoredOpenFlareToken(user.token);
|
||||
|
||||
@@ -4,7 +4,7 @@ import {zodResolver} from '@hookform/resolvers/zod';
|
||||
import {useMutation, useQuery} from '@tanstack/react-query';
|
||||
import Link from 'next/link';
|
||||
import {useRouter, useSearchParams} from 'next/navigation';
|
||||
import {useState} from 'react';
|
||||
import {useEffect, useRef, useState} from 'react';
|
||||
import {useForm} from 'react-hook-form';
|
||||
import {z} from 'zod';
|
||||
|
||||
@@ -16,6 +16,22 @@ import {getPublicStatus} from '@/features/auth/api/public';
|
||||
import {AuthButton, AuthFormField, AuthInput, SecondaryButton,} from '@/features/auth/components/auth-form-primitives';
|
||||
import {PublicAuthGuard} from '@/features/auth/components/public-auth-guard';
|
||||
|
||||
declare module 'react' {
|
||||
/* eslint-disable-next-line @typescript-eslint/no-namespace */
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
'cap-widget': React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLElement> & {
|
||||
ref?: React.RefObject<unknown> | React.Ref<unknown>;
|
||||
onsolve?: (e: CustomEvent<{ token: string }>) => void;
|
||||
'data-cap-api-endpoint'?: string;
|
||||
},
|
||||
HTMLElement
|
||||
>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const TEXT = {
|
||||
usernameRequired: '\u8bf7\u8f93\u5165\u7528\u6237\u540d',
|
||||
passwordRequired: '\u8bf7\u8f93\u5165\u5bc6\u7801',
|
||||
@@ -43,6 +59,8 @@ export function LoginForm() {
|
||||
const searchParams = useSearchParams();
|
||||
const { setUser } = useAuth();
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [capToken, setCapToken] = useState('');
|
||||
const capWidgetRef = useRef<HTMLElement & { reset?: () => void }>(null);
|
||||
const redirect = searchParams?.get('redirect') || '/';
|
||||
|
||||
const form = useForm<LoginFormValues>({
|
||||
@@ -58,6 +76,19 @@ export function LoginForm() {
|
||||
queryFn: getPublicStatus,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (statusQuery.data?.cap_login_enabled) {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/cap-widget';
|
||||
script.type = 'module';
|
||||
script.async = true;
|
||||
document.head.appendChild(script);
|
||||
return () => {
|
||||
document.head.removeChild(script);
|
||||
};
|
||||
}
|
||||
}, [statusQuery.data?.cap_login_enabled]);
|
||||
|
||||
const loginMutation = useMutation({
|
||||
mutationFn: login,
|
||||
onSuccess: (user) => {
|
||||
@@ -66,6 +97,10 @@ export function LoginForm() {
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
setErrorMessage(error.message || TEXT.loginFailed);
|
||||
setCapToken('');
|
||||
if (capWidgetRef.current && typeof capWidgetRef.current.reset === 'function') {
|
||||
capWidgetRef.current.reset();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -81,7 +116,10 @@ export function LoginForm() {
|
||||
|
||||
const handleSubmit = form.handleSubmit((values) => {
|
||||
setErrorMessage('');
|
||||
loginMutation.mutate(values);
|
||||
loginMutation.mutate({
|
||||
...values,
|
||||
cap_token: capToken || undefined,
|
||||
});
|
||||
});
|
||||
|
||||
const handleOAuthLogin = (sourceName: string) => {
|
||||
@@ -118,12 +156,27 @@ export function LoginForm() {
|
||||
) : null}
|
||||
</AuthFormField>
|
||||
|
||||
{statusQuery.data?.cap_login_enabled ? (
|
||||
<div className="flex justify-center py-2">
|
||||
<cap-widget
|
||||
ref={capWidgetRef}
|
||||
data-cap-api-endpoint="/api/cap/"
|
||||
onsolve={(e: CustomEvent<{ token: string }>) => {
|
||||
setCapToken(e.detail.token);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{errorMessage ? (
|
||||
<InlineMessage tone="danger" message={errorMessage} />
|
||||
) : null}
|
||||
|
||||
<div>
|
||||
<AuthButton type="submit" disabled={loginMutation.isPending}>
|
||||
<AuthButton
|
||||
type="submit"
|
||||
disabled={loginMutation.isPending || (statusQuery.data?.cap_login_enabled && !capToken)}
|
||||
>
|
||||
{loginMutation.isPending ? TEXT.loginPending : TEXT.login}
|
||||
</AuthButton>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
|
||||
import {useEffect, useMemo, useState} from 'react';
|
||||
|
||||
import { EmptyState } from '@/components/feedback/empty-state';
|
||||
import { ErrorState } from '@/components/feedback/error-state';
|
||||
import { InlineMessage } from '@/components/feedback/inline-message';
|
||||
import { LoadingState } from '@/components/feedback/loading-state';
|
||||
import { AppModal } from '@/components/ui/app-modal';
|
||||
import { useAuth } from '@/components/providers/auth-provider';
|
||||
import { PageHeader } from '@/components/layout/page-header';
|
||||
import { AppCard } from '@/components/ui/app-card';
|
||||
import { StatusBadge } from '@/components/ui/status-badge';
|
||||
import {
|
||||
getOAuthAuthorizeUrl,
|
||||
sendEmailVerification,
|
||||
} from '@/features/auth/api/auth';
|
||||
import { getPublicStatus } from '@/features/auth/api/public';
|
||||
import {EmptyState} from '@/components/feedback/empty-state';
|
||||
import {ErrorState} from '@/components/feedback/error-state';
|
||||
import {InlineMessage} from '@/components/feedback/inline-message';
|
||||
import {LoadingState} from '@/components/feedback/loading-state';
|
||||
import {AppModal} from '@/components/ui/app-modal';
|
||||
import {useAuth} from '@/components/providers/auth-provider';
|
||||
import {PageHeader} from '@/components/layout/page-header';
|
||||
import {AppCard} from '@/components/ui/app-card';
|
||||
import {StatusBadge} from '@/components/ui/status-badge';
|
||||
import {getOAuthAuthorizeUrl, sendEmailVerification,} from '@/features/auth/api/auth';
|
||||
import {getPublicStatus} from '@/features/auth/api/public';
|
||||
import {
|
||||
bindEmail,
|
||||
cleanupDatabaseObservability,
|
||||
@@ -29,12 +26,12 @@ import {
|
||||
getSettingsProfile,
|
||||
lookupGeoIP,
|
||||
rotateBootstrapToken,
|
||||
syncUptimeKuma,
|
||||
updateOptions,
|
||||
updateSelf,
|
||||
syncUptimeKuma,
|
||||
} from '@/features/settings/api/settings';
|
||||
import { AuthSourceModal } from '@/features/settings/components/auth-source-modal';
|
||||
import { UptimeKumaSiteSelectModal } from './uptimekuma-modal';
|
||||
import {AuthSourceModal} from '@/features/settings/components/auth-source-modal';
|
||||
import {UptimeKumaSiteSelectModal} from './uptimekuma-modal';
|
||||
import type {
|
||||
BootstrapTokenPayload,
|
||||
DatabaseCleanupResult,
|
||||
@@ -54,7 +51,7 @@ import {
|
||||
SecondaryButton,
|
||||
ToggleField,
|
||||
} from '@/features/shared/components/resource-primitives';
|
||||
import { formatDateTime } from '@/lib/utils/date';
|
||||
import {formatDateTime} from '@/lib/utils/date';
|
||||
|
||||
const settingsQueryKey = ['settings', 'options'] as const;
|
||||
const authSourcesQueryKey = ['settings', 'auth-sources'] as const;
|
||||
@@ -68,6 +65,7 @@ const installerScriptUrl =
|
||||
const defaultSystemFields = {
|
||||
ServerAddress: '',
|
||||
PasswordLoginEnabled: true,
|
||||
CapLoginEnabled: true,
|
||||
PasswordRegisterEnabled: true,
|
||||
EmailVerificationEnabled: false,
|
||||
GitHubOAuthEnabled: false,
|
||||
@@ -358,6 +356,7 @@ export function SettingsPage() {
|
||||
setSystemFields({
|
||||
ServerAddress: resolvedServerAddress,
|
||||
PasswordLoginEnabled: toBoolean(optionMap.PasswordLoginEnabled, true),
|
||||
CapLoginEnabled: toBoolean(optionMap.CapLoginEnabled, true),
|
||||
PasswordRegisterEnabled: toBoolean(
|
||||
optionMap.PasswordRegisterEnabled,
|
||||
true,
|
||||
@@ -1841,6 +1840,15 @@ export function SettingsPage() {
|
||||
}
|
||||
disabled={busyKey === 'toggle-PasswordLoginEnabled'}
|
||||
/>
|
||||
<ToggleField
|
||||
label="启用登录人机验证"
|
||||
description="开启后,密码登录前必须完成 Proof-of-Work 人机验证,防止暴力破解。"
|
||||
checked={systemFields.CapLoginEnabled}
|
||||
onChange={(checked) =>
|
||||
handleToggleOption('CapLoginEnabled', checked)
|
||||
}
|
||||
disabled={busyKey === 'toggle-CapLoginEnabled'}
|
||||
/>
|
||||
<ToggleField
|
||||
label="允许密码注册"
|
||||
description="关闭后新用户不能通过密码方式注册。"
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface AuthUser {
|
||||
export interface LoginPayload {
|
||||
username: string;
|
||||
password: string;
|
||||
cap_token?: string;
|
||||
}
|
||||
|
||||
export interface RegisterPayload {
|
||||
|
||||
@@ -20,5 +20,6 @@ export interface PublicStatus {
|
||||
wechat_login: boolean;
|
||||
server_address: string;
|
||||
password_register_enabled: boolean;
|
||||
cap_login_enabled?: boolean;
|
||||
auth_sources: PublicAuthSource[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user