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"
|
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 = os.environ.get(
|
||||||
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
|
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -441,6 +441,14 @@ from open_webui.env import (
|
||||||
WEBUI_SESSION_COOKIE_SAME_SITE,
|
WEBUI_SESSION_COOKIE_SAME_SITE,
|
||||||
WEBUI_SESSION_COOKIE_SECURE,
|
WEBUI_SESSION_COOKIE_SECURE,
|
||||||
ENABLE_SIGNUP_PASSWORD_CONFIRMATION,
|
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_EMAIL_HEADER,
|
||||||
WEBUI_AUTH_TRUSTED_NAME_HEADER,
|
WEBUI_AUTH_TRUSTED_NAME_HEADER,
|
||||||
WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
|
WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
|
||||||
|
|
@ -465,6 +473,7 @@ from open_webui.utils.models import (
|
||||||
check_model_access,
|
check_model_access,
|
||||||
get_filtered_models,
|
get_filtered_models,
|
||||||
)
|
)
|
||||||
|
from open_webui.utils.email_utils import EmailVerificationManager
|
||||||
from open_webui.utils.chat import (
|
from open_webui.utils.chat import (
|
||||||
generate_chat_completion as chat_completion_handler,
|
generate_chat_completion as chat_completion_handler,
|
||||||
chat_completed as chat_completed_handler,
|
chat_completed as chat_completed_handler,
|
||||||
|
|
@ -571,6 +580,7 @@ async def lifespan(app: FastAPI):
|
||||||
redis_cluster=REDIS_CLUSTER,
|
redis_cluster=REDIS_CLUSTER,
|
||||||
async_mode=True,
|
async_mode=True,
|
||||||
)
|
)
|
||||||
|
app.state.email_verification_manager = EmailVerificationManager(app.state.redis)
|
||||||
|
|
||||||
if app.state.redis is not None:
|
if app.state.redis is not None:
|
||||||
app.state.redis_task_command_listener = asyncio.create_task(
|
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.SHOW_ADMIN_DETAILS = SHOW_ADMIN_DETAILS
|
||||||
app.state.config.ADMIN_EMAIL = ADMIN_EMAIL
|
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_MODELS = DEFAULT_MODELS
|
||||||
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
||||||
|
|
@ -1870,26 +1894,28 @@ async def get_app_config(request: Request):
|
||||||
|
|
||||||
return {
|
return {
|
||||||
**({"onboarding": True} if onboarding else {}),
|
**({"onboarding": True} if onboarding else {}),
|
||||||
"status": True,
|
"status": True,
|
||||||
"name": app.state.WEBUI_NAME,
|
"name": app.state.WEBUI_NAME,
|
||||||
"version": VERSION,
|
"version": VERSION,
|
||||||
"default_locale": str(DEFAULT_LOCALE),
|
"default_locale": str(DEFAULT_LOCALE),
|
||||||
"oauth": {
|
"oauth": {
|
||||||
"providers": {
|
"providers": {
|
||||||
name: config.get("name", name)
|
name: config.get("name", name)
|
||||||
for name, config in OAUTH_PROVIDERS.items()
|
for name, config in OAUTH_PROVIDERS.items()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"features": {
|
"features": {
|
||||||
"auth": WEBUI_AUTH,
|
"auth": WEBUI_AUTH,
|
||||||
"auth_trusted_header": bool(app.state.AUTH_TRUSTED_EMAIL_HEADER),
|
"auth_trusted_header": bool(app.state.AUTH_TRUSTED_EMAIL_HEADER),
|
||||||
"enable_signup_password_confirmation": ENABLE_SIGNUP_PASSWORD_CONFIRMATION,
|
"enable_signup_password_confirmation": ENABLE_SIGNUP_PASSWORD_CONFIRMATION,
|
||||||
"enable_ldap": app.state.config.ENABLE_LDAP,
|
"enable_signup_email_verification": True,
|
||||||
"enable_api_key": app.state.config.ENABLE_API_KEY,
|
"enable_ldap": app.state.config.ENABLE_LDAP,
|
||||||
"enable_signup": app.state.config.ENABLE_SIGNUP,
|
"enable_api_key": app.state.config.ENABLE_API_KEY,
|
||||||
"enable_login_form": app.state.config.ENABLE_LOGIN_FORM,
|
"enable_signup": app.state.config.ENABLE_SIGNUP,
|
||||||
"enable_websocket": ENABLE_WEBSOCKET_SUPPORT,
|
"enable_login_form": app.state.config.ENABLE_LOGIN_FORM,
|
||||||
"enable_version_update_check": ENABLE_VERSION_UPDATE_CHECK,
|
"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,
|
"enable_direct_connections": app.state.config.ENABLE_DIRECT_CONNECTIONS,
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,11 @@ class SignupForm(BaseModel):
|
||||||
email: str
|
email: str
|
||||||
password: str
|
password: str
|
||||||
profile_image_url: Optional[str] = "/user.png"
|
profile_image_url: Optional[str] = "/user.png"
|
||||||
|
code: str
|
||||||
|
|
||||||
|
|
||||||
|
class SignupCodeForm(BaseModel):
|
||||||
|
email: str
|
||||||
|
|
||||||
|
|
||||||
class AddUserForm(SignupForm):
|
class AddUserForm(SignupForm):
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ from open_webui.models.auths import (
|
||||||
SigninForm,
|
SigninForm,
|
||||||
SigninResponse,
|
SigninResponse,
|
||||||
SignupForm,
|
SignupForm,
|
||||||
|
SignupCodeForm,
|
||||||
UpdatePasswordForm,
|
UpdatePasswordForm,
|
||||||
UserResponse,
|
UserResponse,
|
||||||
)
|
)
|
||||||
|
|
@ -30,6 +31,14 @@ from open_webui.env import (
|
||||||
WEBUI_AUTH_COOKIE_SAME_SITE,
|
WEBUI_AUTH_COOKIE_SAME_SITE,
|
||||||
WEBUI_AUTH_COOKIE_SECURE,
|
WEBUI_AUTH_COOKIE_SECURE,
|
||||||
WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
|
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,
|
ENABLE_INITIAL_ADMIN_SIGNUP,
|
||||||
SRC_LOG_LEVELS,
|
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 pydantic import BaseModel
|
||||||
|
|
||||||
from open_webui.utils.misc import parse_duration, validate_email_format
|
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 (
|
from open_webui.utils.auth import (
|
||||||
decode_token,
|
decode_token,
|
||||||
create_api_key,
|
create_api_key,
|
||||||
|
|
@ -561,6 +575,91 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
|
||||||
# SignUp
|
# 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)
|
@router.post("/signup", response_model=SessionUserResponse)
|
||||||
async def signup(request: Request, response: Response, form_data: SignupForm):
|
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
|
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(
|
raise HTTPException(
|
||||||
status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
|
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)
|
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:
|
try:
|
||||||
role = "admin" if not has_users else "user"
|
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)
|
hashed = get_password_hash(form_data.password)
|
||||||
user = Auths.insert_new_auth(
|
user = Auths.insert_new_auth(
|
||||||
form_data.email.lower(),
|
email,
|
||||||
hashed,
|
hashed,
|
||||||
form_data.name,
|
form_data.name,
|
||||||
form_data.profile_image_url,
|
form_data.profile_image_url,
|
||||||
|
|
|
||||||
|
|
@ -290,7 +290,8 @@ export const userSignUp = async (
|
||||||
name: string,
|
name: string,
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
profile_image_url: string
|
profile_image_url: string,
|
||||||
|
code: string
|
||||||
) => {
|
) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|
@ -304,7 +305,8 @@ export const userSignUp = async (
|
||||||
name: name,
|
name: name,
|
||||||
email: email,
|
email: email,
|
||||||
password: password,
|
password: password,
|
||||||
profile_image_url: profile_image_url
|
profile_image_url: profile_image_url,
|
||||||
|
code
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
|
|
@ -324,6 +326,33 @@ export const userSignUp = async (
|
||||||
return res;
|
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 () => {
|
export const userSignOut = async () => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -543,7 +543,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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 -->
|
<!-- Add private model button -->
|
||||||
<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"
|
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_api_key: boolean;
|
||||||
enable_signup: boolean;
|
enable_signup: boolean;
|
||||||
enable_login_form: 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_web_search?: boolean;
|
||||||
enable_google_drive_integration: boolean;
|
enable_google_drive_integration: boolean;
|
||||||
enable_onedrive_integration: boolean;
|
enable_onedrive_integration: boolean;
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,18 @@
|
||||||
|
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
import { onMount, getContext, tick } from 'svelte';
|
import { onMount, getContext, tick, onDestroy } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
import { getBackendConfig } from '$lib/apis';
|
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 { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||||
import { agreementContent as defaultAgreementContent, privacyContent as defaultPrivacyContent } from '$lib/constants/legal';
|
import { agreementContent as defaultAgreementContent, privacyContent as defaultPrivacyContent } from '$lib/constants/legal';
|
||||||
|
|
@ -35,6 +41,10 @@
|
||||||
let email = '';
|
let email = '';
|
||||||
let password = '';
|
let password = '';
|
||||||
let confirmPassword = '';
|
let confirmPassword = '';
|
||||||
|
let verificationCode = '';
|
||||||
|
let sendCodeCooldown = 0;
|
||||||
|
let sendCodeTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
let sendingCode = false;
|
||||||
|
|
||||||
let ldapUsername = '';
|
let ldapUsername = '';
|
||||||
let agreeToTerms = false;
|
let agreeToTerms = false;
|
||||||
|
|
@ -81,12 +91,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionUser = await userSignUp(name, email, password, generateInitialsImage(name)).catch(
|
if (!verificationCode) {
|
||||||
(error) => {
|
toast.error('请输入邮箱验证码');
|
||||||
toast.error(`${error}`);
|
return;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
);
|
const sessionUser = await userSignUp(
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
generateInitialsImage(name),
|
||||||
|
verificationCode
|
||||||
|
).catch((error) => {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
await setSessionUser(sessionUser);
|
await setSessionUser(sessionUser);
|
||||||
};
|
};
|
||||||
|
|
@ -99,6 +118,51 @@
|
||||||
await setSessionUser(sessionUser);
|
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 () => {
|
const submitHandler = async () => {
|
||||||
if (mode === 'ldap') {
|
if (mode === 'ldap') {
|
||||||
await ldapSignInHandler();
|
await ldapSignInHandler();
|
||||||
|
|
@ -196,6 +260,10 @@
|
||||||
onboarding = $config?.onboarding ?? false;
|
onboarding = $config?.onboarding ?? false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
clearSendCodeTimer();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
@ -332,6 +400,37 @@
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</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}
|
{/if}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue