mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-13 12:55:19 +00:00
- 注册强制邮箱验证码:后端新增验证码发送/校验链路,/api/auths/signup/code 发送验证码,/api/auths/signup 必填 code 校验后才创建用户(首个用户亦需验证码)。
- 邮件发送改为固定 SMTPS(SSL):新增 backend/open_webui/utils/email_utils.py,使用 SMTP_SSL 直连;去掉 TLS 开关,SMTP 配置只含 server/port/username/password/from。 - 配置改动:SMTP 相关环境变量仅需设定服务器、端口、账户、密码、发信人。 - 前端注册页:验证码输入与发送按钮始终展示;验证码必填;新增 sendSignupCode API 调用;userSignUp 必传验证码。
This commit is contained in:
parent
29d2b0792a
commit
e10f795789
8 changed files with 365 additions and 29 deletions
|
|
@ -417,6 +417,59 @@ ENABLE_SIGNUP_PASSWORD_CONFIRMATION = (
|
|||
os.environ.get("ENABLE_SIGNUP_PASSWORD_CONFIRMATION", "False").lower() == "true"
|
||||
)
|
||||
|
||||
# 注册邮箱验证码相关配置(强制开启,不再提供开关)
|
||||
# - EMAIL_SMTP_SERVER:SMTP 服务器地址,必填
|
||||
# - EMAIL_SMTP_PORT:SMTP 端口(默认 587)
|
||||
# - EMAIL_SMTP_USERNAME:SMTP 登录用户名
|
||||
# - EMAIL_SMTP_PASSWORD:SMTP 登录密码
|
||||
# - EMAIL_SMTP_FROM:发信人地址(默认 no-reply@localhost)
|
||||
# - EMAIL_VERIFICATION_CODE_TTL:验证码有效期秒数(默认 600)
|
||||
# - EMAIL_VERIFICATION_SEND_INTERVAL:同邮箱再次发送的冷却秒数(默认 60)
|
||||
# - EMAIL_VERIFICATION_MAX_ATTEMPTS:验证码最大验证次数,超限需重发(默认 5)
|
||||
ENABLE_SIGNUP_EMAIL_VERIFICATION = True
|
||||
|
||||
# SMTP 配置:用于发送注册验证码
|
||||
# 服务器地址(开启验证码时必填)
|
||||
EMAIL_SMTP_SERVER = os.environ.get("EMAIL_SMTP_SERVER", "")
|
||||
# 端口,默认 587
|
||||
EMAIL_SMTP_PORT = os.environ.get("EMAIL_SMTP_PORT", "587")
|
||||
try:
|
||||
EMAIL_SMTP_PORT = int(EMAIL_SMTP_PORT)
|
||||
except ValueError:
|
||||
EMAIL_SMTP_PORT = 587
|
||||
|
||||
EMAIL_SMTP_USERNAME = os.environ.get("EMAIL_SMTP_USERNAME", "")
|
||||
EMAIL_SMTP_PASSWORD = os.environ.get("EMAIL_SMTP_PASSWORD", "")
|
||||
# 发信人地址
|
||||
EMAIL_SMTP_FROM = os.environ.get("EMAIL_SMTP_FROM", "no-reply@localhost")
|
||||
|
||||
# 验证码策略:有效期、发送冷却、最大尝试次数
|
||||
# 有效期(秒)
|
||||
EMAIL_VERIFICATION_CODE_TTL = os.environ.get("EMAIL_VERIFICATION_CODE_TTL", "600")
|
||||
try:
|
||||
EMAIL_VERIFICATION_CODE_TTL = int(EMAIL_VERIFICATION_CODE_TTL)
|
||||
except ValueError:
|
||||
EMAIL_VERIFICATION_CODE_TTL = 600
|
||||
|
||||
# 发送冷却(秒)
|
||||
EMAIL_VERIFICATION_SEND_INTERVAL = os.environ.get(
|
||||
"EMAIL_VERIFICATION_SEND_INTERVAL", "60"
|
||||
)
|
||||
try:
|
||||
EMAIL_VERIFICATION_SEND_INTERVAL = int(EMAIL_VERIFICATION_SEND_INTERVAL)
|
||||
except ValueError:
|
||||
EMAIL_VERIFICATION_SEND_INTERVAL = 60
|
||||
|
||||
# 最大验证尝试次数
|
||||
EMAIL_VERIFICATION_MAX_ATTEMPTS = os.environ.get(
|
||||
"EMAIL_VERIFICATION_MAX_ATTEMPTS", "5"
|
||||
)
|
||||
try:
|
||||
EMAIL_VERIFICATION_MAX_ATTEMPTS = int(EMAIL_VERIFICATION_MAX_ATTEMPTS)
|
||||
except ValueError:
|
||||
EMAIL_VERIFICATION_MAX_ATTEMPTS = 5
|
||||
|
||||
|
||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
|
||||
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
|
||||
)
|
||||
|
|
|
|||
|
|
@ -441,6 +441,14 @@ from open_webui.env import (
|
|||
WEBUI_SESSION_COOKIE_SAME_SITE,
|
||||
WEBUI_SESSION_COOKIE_SECURE,
|
||||
ENABLE_SIGNUP_PASSWORD_CONFIRMATION,
|
||||
EMAIL_SMTP_SERVER,
|
||||
EMAIL_SMTP_PORT,
|
||||
EMAIL_SMTP_USERNAME,
|
||||
EMAIL_SMTP_PASSWORD,
|
||||
EMAIL_SMTP_FROM,
|
||||
EMAIL_VERIFICATION_CODE_TTL,
|
||||
EMAIL_VERIFICATION_SEND_INTERVAL,
|
||||
EMAIL_VERIFICATION_MAX_ATTEMPTS,
|
||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
|
||||
WEBUI_AUTH_TRUSTED_NAME_HEADER,
|
||||
WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
|
||||
|
|
@ -465,6 +473,7 @@ from open_webui.utils.models import (
|
|||
check_model_access,
|
||||
get_filtered_models,
|
||||
)
|
||||
from open_webui.utils.email_utils import EmailVerificationManager
|
||||
from open_webui.utils.chat import (
|
||||
generate_chat_completion as chat_completion_handler,
|
||||
chat_completed as chat_completed_handler,
|
||||
|
|
@ -571,6 +580,7 @@ async def lifespan(app: FastAPI):
|
|||
redis_cluster=REDIS_CLUSTER,
|
||||
async_mode=True,
|
||||
)
|
||||
app.state.email_verification_manager = EmailVerificationManager(app.state.redis)
|
||||
|
||||
if app.state.redis is not None:
|
||||
app.state.redis_task_command_listener = asyncio.create_task(
|
||||
|
|
@ -733,6 +743,20 @@ app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
|
|||
app.state.config.SHOW_ADMIN_DETAILS = SHOW_ADMIN_DETAILS
|
||||
app.state.config.ADMIN_EMAIL = ADMIN_EMAIL
|
||||
|
||||
app.state.email_verification_enabled = True
|
||||
app.state.email_verification_config = {
|
||||
"ttl": EMAIL_VERIFICATION_CODE_TTL,
|
||||
"send_interval": EMAIL_VERIFICATION_SEND_INTERVAL,
|
||||
"max_attempts": EMAIL_VERIFICATION_MAX_ATTEMPTS,
|
||||
"smtp": {
|
||||
"server": EMAIL_SMTP_SERVER,
|
||||
"port": EMAIL_SMTP_PORT,
|
||||
"username": EMAIL_SMTP_USERNAME,
|
||||
"password": EMAIL_SMTP_PASSWORD,
|
||||
"from_email": EMAIL_SMTP_FROM,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
|
||||
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
||||
|
|
@ -1884,12 +1908,14 @@ async def get_app_config(request: Request):
|
|||
"auth": WEBUI_AUTH,
|
||||
"auth_trusted_header": bool(app.state.AUTH_TRUSTED_EMAIL_HEADER),
|
||||
"enable_signup_password_confirmation": ENABLE_SIGNUP_PASSWORD_CONFIRMATION,
|
||||
"enable_signup_email_verification": True,
|
||||
"enable_ldap": app.state.config.ENABLE_LDAP,
|
||||
"enable_api_key": app.state.config.ENABLE_API_KEY,
|
||||
"enable_signup": app.state.config.ENABLE_SIGNUP,
|
||||
"enable_login_form": app.state.config.ENABLE_LOGIN_FORM,
|
||||
"enable_websocket": ENABLE_WEBSOCKET_SUPPORT,
|
||||
"enable_version_update_check": ENABLE_VERSION_UPDATE_CHECK,
|
||||
"signup_email_verification_send_interval": app.state.email_verification_config["send_interval"],
|
||||
**(
|
||||
{
|
||||
"enable_direct_connections": app.state.config.ENABLE_DIRECT_CONNECTIONS,
|
||||
|
|
|
|||
|
|
@ -83,6 +83,11 @@ class SignupForm(BaseModel):
|
|||
email: str
|
||||
password: str
|
||||
profile_image_url: Optional[str] = "/user.png"
|
||||
code: str
|
||||
|
||||
|
||||
class SignupCodeForm(BaseModel):
|
||||
email: str
|
||||
|
||||
|
||||
class AddUserForm(SignupForm):
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from open_webui.models.auths import (
|
|||
SigninForm,
|
||||
SigninResponse,
|
||||
SignupForm,
|
||||
SignupCodeForm,
|
||||
UpdatePasswordForm,
|
||||
UserResponse,
|
||||
)
|
||||
|
|
@ -30,6 +31,14 @@ from open_webui.env import (
|
|||
WEBUI_AUTH_COOKIE_SAME_SITE,
|
||||
WEBUI_AUTH_COOKIE_SECURE,
|
||||
WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
|
||||
EMAIL_VERIFICATION_CODE_TTL,
|
||||
EMAIL_VERIFICATION_SEND_INTERVAL,
|
||||
EMAIL_VERIFICATION_MAX_ATTEMPTS,
|
||||
EMAIL_SMTP_SERVER,
|
||||
EMAIL_SMTP_PORT,
|
||||
EMAIL_SMTP_USERNAME,
|
||||
EMAIL_SMTP_PASSWORD,
|
||||
EMAIL_SMTP_FROM,
|
||||
ENABLE_INITIAL_ADMIN_SIGNUP,
|
||||
SRC_LOG_LEVELS,
|
||||
)
|
||||
|
|
@ -39,6 +48,11 @@ from open_webui.config import OPENID_PROVIDER_URL, ENABLE_OAUTH_SIGNUP, ENABLE_L
|
|||
from pydantic import BaseModel
|
||||
|
||||
from open_webui.utils.misc import parse_duration, validate_email_format
|
||||
from open_webui.utils.email_utils import (
|
||||
EmailVerificationManager,
|
||||
generate_verification_code,
|
||||
send_email,
|
||||
)
|
||||
from open_webui.utils.auth import (
|
||||
decode_token,
|
||||
create_api_key,
|
||||
|
|
@ -561,6 +575,91 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
|
|||
# SignUp
|
||||
############################
|
||||
|
||||
@router.post("/signup/code")
|
||||
async def send_signup_code(request: Request, form_data: SignupCodeForm):
|
||||
has_users = Users.has_users()
|
||||
|
||||
if WEBUI_AUTH:
|
||||
if (
|
||||
not request.app.state.config.ENABLE_SIGNUP
|
||||
or not request.app.state.config.ENABLE_LOGIN_FORM
|
||||
):
|
||||
if has_users or not ENABLE_INITIAL_ADMIN_SIGNUP:
|
||||
raise HTTPException(
|
||||
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
|
||||
)
|
||||
else:
|
||||
if has_users:
|
||||
raise HTTPException(
|
||||
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
|
||||
)
|
||||
|
||||
email = form_data.email.lower()
|
||||
|
||||
if not validate_email_format(email):
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
|
||||
)
|
||||
|
||||
manager: EmailVerificationManager = getattr(
|
||||
request.app.state, "email_verification_manager", None
|
||||
)
|
||||
if manager is None:
|
||||
manager = EmailVerificationManager(request.app.state.redis)
|
||||
request.app.state.email_verification_manager = manager
|
||||
|
||||
send_interval = request.app.state.email_verification_config["send_interval"]
|
||||
can_send, remaining = manager.can_send(email, send_interval)
|
||||
if not can_send:
|
||||
raise HTTPException(
|
||||
status.HTTP_429_TOO_MANY_REQUESTS,
|
||||
detail=f"Please wait {int(remaining)} seconds before requesting a new code.",
|
||||
)
|
||||
|
||||
ttl = request.app.state.email_verification_config["ttl"]
|
||||
max_attempts = request.app.state.email_verification_config["max_attempts"]
|
||||
smtp_config = request.app.state.email_verification_config.get("smtp", {})
|
||||
|
||||
print(smtp_config)
|
||||
|
||||
if not smtp_config.get("server"):
|
||||
raise HTTPException(
|
||||
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Email service is not configured.",
|
||||
)
|
||||
|
||||
code = generate_verification_code()
|
||||
manager.store_code(
|
||||
email,
|
||||
code,
|
||||
ttl,
|
||||
max_attempts,
|
||||
request.client.host if request.client else None,
|
||||
)
|
||||
|
||||
try:
|
||||
send_email(
|
||||
subject=f"{request.app.state.WEBUI_NAME} Verification Code",
|
||||
body=(
|
||||
f"Your verification code is {code}.\n"
|
||||
f"It expires in {max(1, int(ttl / 60))} minutes."
|
||||
),
|
||||
to_email=email,
|
||||
smtp_server=smtp_config.get("server", ""),
|
||||
smtp_port=int(smtp_config.get("port", 587)),
|
||||
smtp_username=smtp_config.get("username", ""),
|
||||
smtp_password=smtp_config.get("password", ""),
|
||||
from_email=smtp_config.get("from_email", EMAIL_SMTP_FROM),
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(f"Failed to send verification email: {e}")
|
||||
raise HTTPException(
|
||||
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to send verification email.",
|
||||
)
|
||||
|
||||
return {"status": True}
|
||||
|
||||
|
||||
@router.post("/signup", response_model=SessionUserResponse)
|
||||
async def signup(request: Request, response: Response, form_data: SignupForm):
|
||||
|
|
@ -581,14 +680,36 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
|
|||
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
|
||||
)
|
||||
|
||||
if not validate_email_format(form_data.email.lower()):
|
||||
email = form_data.email.lower()
|
||||
|
||||
if not validate_email_format(email):
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
|
||||
)
|
||||
|
||||
if Users.get_user_by_email(form_data.email.lower()):
|
||||
if Users.get_user_by_email(email):
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
|
||||
|
||||
if form_data.code is None:
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
detail="Verification code is required.",
|
||||
)
|
||||
|
||||
if form_data.code:
|
||||
manager: EmailVerificationManager = getattr(
|
||||
request.app.state, "email_verification_manager", None
|
||||
)
|
||||
if manager is None:
|
||||
manager = EmailVerificationManager(request.app.state.redis)
|
||||
request.app.state.email_verification_manager = manager
|
||||
|
||||
if not manager.validate_code(email, form_data.code):
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid or expired verification code.",
|
||||
)
|
||||
|
||||
try:
|
||||
role = "admin" if not has_users else "user"
|
||||
|
||||
|
|
@ -601,7 +722,7 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
|
|||
|
||||
hashed = get_password_hash(form_data.password)
|
||||
user = Auths.insert_new_auth(
|
||||
form_data.email.lower(),
|
||||
email,
|
||||
hashed,
|
||||
form_data.name,
|
||||
form_data.profile_image_url,
|
||||
|
|
|
|||
|
|
@ -290,7 +290,8 @@ export const userSignUp = async (
|
|||
name: string,
|
||||
email: string,
|
||||
password: string,
|
||||
profile_image_url: string
|
||||
profile_image_url: string,
|
||||
code: string
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
|
|
@ -304,7 +305,8 @@ export const userSignUp = async (
|
|||
name: name,
|
||||
email: email,
|
||||
password: password,
|
||||
profile_image_url: profile_image_url
|
||||
profile_image_url: profile_image_url,
|
||||
code
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
|
|
@ -324,6 +326,33 @@ export const userSignUp = async (
|
|||
return res;
|
||||
};
|
||||
|
||||
export const sendSignupCode = async (email: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup/code`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email })
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
error = err.detail ?? err.message ?? err;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const userSignOut = async () => {
|
||||
let error = null;
|
||||
|
||||
|
|
|
|||
|
|
@ -543,7 +543,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="px-2.5 max-h-64 overflow-y-auto group relative">
|
||||
<div class="px-2.5 max-h-128 overflow-y-auto group relative">
|
||||
<!-- Add private model button -->
|
||||
<button
|
||||
class="w-full text-left px-2 py-1.5 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800/60 flex items-center gap-2 transition mb-1"
|
||||
|
|
|
|||
|
|
@ -256,6 +256,9 @@ type Config = {
|
|||
enable_api_key: boolean;
|
||||
enable_signup: boolean;
|
||||
enable_login_form: boolean;
|
||||
enable_signup_email_verification?: boolean;
|
||||
enable_signup_password_confirmation?: boolean;
|
||||
signup_email_verification_send_interval?: number;
|
||||
enable_web_search?: boolean;
|
||||
enable_google_drive_integration: boolean;
|
||||
enable_onedrive_integration: boolean;
|
||||
|
|
|
|||
|
|
@ -4,12 +4,18 @@
|
|||
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { onMount, getContext, tick } from 'svelte';
|
||||
import { onMount, getContext, tick, onDestroy } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import { getBackendConfig } from '$lib/apis';
|
||||
import { ldapUserSignIn, getSessionUser, userSignIn, userSignUp } from '$lib/apis/auths';
|
||||
import {
|
||||
ldapUserSignIn,
|
||||
getSessionUser,
|
||||
userSignIn,
|
||||
userSignUp,
|
||||
sendSignupCode
|
||||
} from '$lib/apis/auths';
|
||||
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { agreementContent as defaultAgreementContent, privacyContent as defaultPrivacyContent } from '$lib/constants/legal';
|
||||
|
|
@ -35,6 +41,10 @@
|
|||
let email = '';
|
||||
let password = '';
|
||||
let confirmPassword = '';
|
||||
let verificationCode = '';
|
||||
let sendCodeCooldown = 0;
|
||||
let sendCodeTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let sendingCode = false;
|
||||
|
||||
let ldapUsername = '';
|
||||
let agreeToTerms = false;
|
||||
|
|
@ -81,12 +91,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
const sessionUser = await userSignUp(name, email, password, generateInitialsImage(name)).catch(
|
||||
(error) => {
|
||||
if (!verificationCode) {
|
||||
toast.error('请输入邮箱验证码');
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionUser = await userSignUp(
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
generateInitialsImage(name),
|
||||
verificationCode
|
||||
).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await setSessionUser(sessionUser);
|
||||
};
|
||||
|
|
@ -99,6 +118,51 @@
|
|||
await setSessionUser(sessionUser);
|
||||
};
|
||||
|
||||
const clearSendCodeTimer = () => {
|
||||
if (sendCodeTimer) {
|
||||
clearInterval(sendCodeTimer);
|
||||
sendCodeTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const startSendCodeCooldown = (seconds: number) => {
|
||||
clearSendCodeTimer();
|
||||
sendCodeCooldown = seconds;
|
||||
|
||||
sendCodeTimer = setInterval(() => {
|
||||
if (sendCodeCooldown <= 1) {
|
||||
clearSendCodeTimer();
|
||||
sendCodeCooldown = 0;
|
||||
} else {
|
||||
sendCodeCooldown -= 1;
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const sendCodeHandler = async () => {
|
||||
if (!email) {
|
||||
toast.error('请先填写邮箱');
|
||||
return;
|
||||
}
|
||||
|
||||
if (sendCodeCooldown > 0 || sendingCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendingCode = true;
|
||||
const res = await sendSignupCode(email).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
sendingCode = false;
|
||||
|
||||
if (res) {
|
||||
toast.success('验证码已发送,请查收邮箱');
|
||||
const interval = $config?.features?.signup_email_verification_send_interval ?? 60;
|
||||
startSendCodeCooldown(interval);
|
||||
}
|
||||
};
|
||||
|
||||
const submitHandler = async () => {
|
||||
if (mode === 'ldap') {
|
||||
await ldapSignInHandler();
|
||||
|
|
@ -196,6 +260,10 @@
|
|||
onboarding = $config?.onboarding ?? false;
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
clearSendCodeTimer();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -332,6 +400,37 @@
|
|||
required
|
||||
/>
|
||||
</div>
|
||||
{#if mode === 'signup'}
|
||||
<div class="mb-2">
|
||||
<label for="verification-code" class="text-sm font-medium text-left mb-1 block"
|
||||
>邮箱验证码</label
|
||||
>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
bind:value={verificationCode}
|
||||
type="text"
|
||||
id="verification-code"
|
||||
class="my-0.5 w-full text-sm outline-hidden bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-600"
|
||||
placeholder="请输入邮箱验证码"
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 px-3 rounded-full border-1 text-sm font-medium transition-all hover:bg-gray-700/10 dark:hover:bg-gray-100/10"
|
||||
disabled={sendCodeCooldown > 0 || sendingCode}
|
||||
on:click={sendCodeHandler}
|
||||
>
|
||||
{#if sendingCode}
|
||||
发送中...
|
||||
{:else if sendCodeCooldown > 0}
|
||||
{sendCodeCooldown}s
|
||||
{:else}
|
||||
发送验证码
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue