P0 blockers: - Fix migration numbering conflict and duplicate indexes - Change comments.post_id FK to ON DELETE CASCADE - Restrict public post detail endpoint to published posts only - Fix rate-limiting IP extraction and fallback to ConnectInfo - Harden HTML sanitizer: deny unknown URL schemes, restrict data URIs - Remove session token from login response body - Enforce image pixel/dimension limits on upload and serving P1 high-risk: - Validate uploads by magic bytes and decode GIF/WebP - Add pagination/rate-limiting to search, tag posts, and comments - Make first-admin registration and slug uniqueness check atomic - HTML-escape comment author fields - Improve HTML minify cache key and skip admin/error responses - Add mobile navigation menu P2 accessibility/quality: - Associate form labels with inputs - Key PostDetail article by slug to re-init scripts on navigation - Improve image viewer keyboard accessibility - Make theme toggle SSR-friendly and add aria-label - Invalidate slug 404 cache on create and pending count on new comment - Deduplicate tags case-insensitively P3 cleanup: - Remove unused tower-http dependency, expand make clean - Configure DB pool timeouts and verified recycling - Run background cleanup tasks immediately on startup - Use SHA-256 for stable disk cache keys - Log DB errors with Display instead of Debug - Update README migration instructions All tests pass (321), clippy clean, dx check clean.
165 lines
6.4 KiB
Rust
165 lines
6.4 KiB
Rust
//! 注册页面
|
|
//!
|
|
//! 提供新用户注册表单。首个注册成功的用户将自动成为管理员,
|
|
//! 后续注册请求会被服务端拒绝。
|
|
|
|
use dioxus::prelude::*;
|
|
use dioxus::router::components::Link;
|
|
|
|
use crate::api::auth::{register, AuthResponse};
|
|
use crate::components::forms::{AlertBox, FormInput, FormLabel, BUTTON_PRIMARY_CLASS};
|
|
use crate::router::Route;
|
|
|
|
/// 注册页面组件
|
|
#[component]
|
|
pub fn Register() -> Element {
|
|
// 表单输入状态
|
|
let mut username = use_signal(|| "".to_string());
|
|
let mut email = use_signal(|| "".to_string());
|
|
let mut password = use_signal(|| "".to_string());
|
|
let mut confirm_password = use_signal(|| "".to_string());
|
|
// 错误提示、成功提示与加载状态
|
|
let mut error = use_signal(|| None::<String>);
|
|
let mut success = use_signal(|| false);
|
|
let mut loading = use_signal(|| false);
|
|
|
|
// 提交注册表单
|
|
let on_submit = Callback::new(move |_| {
|
|
if loading() {
|
|
return;
|
|
}
|
|
error.set(None);
|
|
success.set(false);
|
|
|
|
// 前端基础校验:密码长度与一致性
|
|
if password().len() < 8 {
|
|
error.set(Some("密码长度至少 8 位".to_string()));
|
|
return;
|
|
}
|
|
if password() != confirm_password() {
|
|
error.set(Some("两次输入的密码不一致".to_string()));
|
|
return;
|
|
}
|
|
|
|
let username_val = username();
|
|
let email_val = email();
|
|
let password_val = password();
|
|
|
|
loading.set(true);
|
|
|
|
// 在异步任务中调用 server function 注册
|
|
spawn(async move {
|
|
match register(username_val, email_val, password_val).await {
|
|
Ok(AuthResponse { success: true, .. }) => {
|
|
success.set(true);
|
|
}
|
|
Ok(AuthResponse {
|
|
success: false,
|
|
message,
|
|
..
|
|
}) => {
|
|
error.set(Some(message));
|
|
}
|
|
Err(e) => {
|
|
error.set(Some(format!("请求失败: {}", e)));
|
|
}
|
|
}
|
|
loading.set(false);
|
|
});
|
|
});
|
|
|
|
let is_loading = loading();
|
|
|
|
rsx! {
|
|
div { class: "min-h-screen flex items-center justify-center bg-paper-theme",
|
|
div { class: "w-full max-w-md p-8 bg-paper-entry rounded-2xl border border-paper-border shadow-sm",
|
|
h1 { class: "text-2xl font-bold text-center text-paper-primary mb-2",
|
|
"注册"
|
|
}
|
|
p { class: "text-sm text-center text-paper-secondary mb-6",
|
|
"首个注册账号将自动成为管理员"
|
|
}
|
|
|
|
if success() {
|
|
div { class: "mb-4 p-3 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-lg text-center",
|
|
"注册成功!"
|
|
Link { class: "block mt-2 text-paper-accent hover:underline cursor-pointer",
|
|
to: Route::Login {},
|
|
"去登录"
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(err) = error() {
|
|
AlertBox { message: err, variant: "error" }
|
|
}
|
|
|
|
div { class: "space-y-4",
|
|
div {
|
|
FormLabel { label: "用户名", html_for: Some("register-username".to_string()) }
|
|
FormInput {
|
|
id: Some("register-username".to_string()),
|
|
r#type: "text",
|
|
placeholder: "3-50 位字符",
|
|
value: username(),
|
|
disabled: is_loading,
|
|
oninput: move |v: String| username.set(v),
|
|
onkeydown: None,
|
|
}
|
|
}
|
|
div {
|
|
FormLabel { label: "邮箱", html_for: Some("register-email".to_string()) }
|
|
FormInput {
|
|
id: Some("register-email".to_string()),
|
|
r#type: "email",
|
|
placeholder: "your@email.com",
|
|
value: email(),
|
|
disabled: is_loading,
|
|
oninput: move |v: String| email.set(v),
|
|
onkeydown: None,
|
|
}
|
|
}
|
|
div {
|
|
FormLabel { label: "密码", html_for: Some("register-password".to_string()) }
|
|
FormInput {
|
|
id: Some("register-password".to_string()),
|
|
r#type: "password",
|
|
placeholder: "至少 8 位",
|
|
value: password(),
|
|
disabled: is_loading,
|
|
oninput: move |v: String| password.set(v),
|
|
onkeydown: None,
|
|
}
|
|
}
|
|
div {
|
|
FormLabel { label: "确认密码", html_for: Some("register-confirm-password".to_string()) }
|
|
FormInput {
|
|
id: Some("register-confirm-password".to_string()),
|
|
r#type: "password",
|
|
placeholder: "再次输入密码",
|
|
value: confirm_password(),
|
|
disabled: is_loading,
|
|
oninput: move |v: String| confirm_password.set(v),
|
|
onkeydown: None,
|
|
}
|
|
}
|
|
button {
|
|
class: "{BUTTON_PRIMARY_CLASS}",
|
|
class: if is_loading { "opacity-60 cursor-not-allowed" },
|
|
disabled: is_loading,
|
|
onclick: move |_| on_submit(()),
|
|
if is_loading { "注册中..." } else { "注册" }
|
|
}
|
|
}
|
|
p { class: "mt-4 text-center text-sm text-paper-secondary",
|
|
"已有账号?"
|
|
Link { class: "text-paper-accent hover:underline cursor-pointer",
|
|
to: Route::Login {},
|
|
"去登录"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|