diff --git a/.gitignore b/.gitignore index 9311e91..d545a22 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ AGENT.md ARCHITECTURE.md .agents .agents.md -.next \ No newline at end of file +.next +admin \ No newline at end of file diff --git a/libs/api/auth/mod.rs b/libs/api/auth/mod.rs index a4fc8d0..ee5b462 100644 --- a/libs/api/auth/mod.rs +++ b/libs/api/auth/mod.rs @@ -5,7 +5,7 @@ pub mod logout; pub mod me; pub mod password; pub mod register; -pub mod totp; +// pub mod totp; // 2FA disabled pub mod ws_token; pub fn init_auth_routes(cfg: &mut actix_web::web::ServiceConfig) { @@ -37,22 +37,12 @@ pub fn init_auth_routes(cfg: &mut actix_web::web::ServiceConfig) { "/password/confirm", actix_web::web::post().to(password::api_user_confirm_password_reset), ) - .route( - "/2fa/enable", - actix_web::web::post().to(totp::api_2fa_enable), - ) - .route( - "/2fa/verify", - actix_web::web::post().to(totp::api_2fa_verify), - ) - .route( - "/2fa/disable", - actix_web::web::post().to(totp::api_2fa_disable), - ) - .route( - "/2fa/status", - actix_web::web::post().to(totp::api_2fa_status), - ) + // 2FA disabled { + // .route("/2fa/enable", actix_web::web::post().to(totp::api_2fa_enable)) + // .route("/2fa/verify", actix_web::web::post().to(totp::api_2fa_verify)) + // .route("/2fa/disable", actix_web::web::post().to(totp::api_2fa_disable)) + // .route("/2fa/status", actix_web::web::post().to(totp::api_2fa_status)) + // } .route("/email", actix_web::web::post().to(email::api_email_get)) .route( "/email/change", diff --git a/libs/api/auth/totp.rs b/libs/api/auth/totp.rs index ed66d2b..555ac02 100644 --- a/libs/api/auth/totp.rs +++ b/libs/api/auth/totp.rs @@ -1,94 +1,96 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::auth::totp::{ - Disable2FAParams, Enable2FAResponse, Get2FAStatusResponse, Verify2FAParams, -}; -use session::Session; - -#[utoipa::path( - post, - path = "/api/auth/2fa/enable", - responses( - (status = 200, description = "2FA setup initiated", body = Enable2FAResponse), - (status = 401, description = "Unauthorized"), - (status = 409, description = "2FA already enabled"), - (status = 500, description = "Internal server error"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Auth" -)] -pub async fn api_2fa_enable( - service: web::Data, - session: Session, -) -> Result { - let resp = service.auth_2fa_enable(&session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/auth/2fa/verify", - request_body = Verify2FAParams, - responses( - (status = 200, description = "2FA verified and enabled"), - (status = 401, description = "Unauthorized or invalid code"), - (status = 400, description = "2FA not set up"), - (status = 500, description = "Internal server error"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Auth" -)] -pub async fn api_2fa_verify( - service: web::Data, - session: Session, - params: web::Json, -) -> Result { - service - .auth_2fa_verify_and_enable(&session, params.into_inner()) - .await?; - Ok(crate::api_success()) -} - -#[utoipa::path( - post, - path = "/api/auth/2fa/disable", - request_body = Disable2FAParams, - responses( - (status = 200, description = "2FA disabled"), - (status = 401, description = "Unauthorized"), - (status = 400, description = "2FA not enabled or invalid code/password"), - (status = 500, description = "Internal server error"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Auth" -)] -pub async fn api_2fa_disable( - service: web::Data, - session: Session, - params: web::Json, -) -> Result { - service - .auth_2fa_disable(&session, params.into_inner()) - .await?; - Ok(crate::api_success()) -} - -#[utoipa::path( - post, - path = "/api/auth/2fa/status", - responses( - (status = 200, description = "2FA status", body = Get2FAStatusResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Auth" -)] -pub async fn api_2fa_status( - service: web::Data, - session: Session, -) -> Result { - let resp = service.auth_2fa_status(&session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} +// 2FA disabled { +// use crate::{ApiResponse, error::ApiError}; +// use actix_web::{HttpResponse, Result, web}; +// use service::AppService; +// use service::auth::totp::{ +// Disable2FAParams, Enable2FAResponse, Get2FAStatusResponse, Verify2FAParams, +// }; +// use session::Session; +// +// #[utoipa::path( +// post, +// path = "/api/auth/2fa/enable", +// responses( +// (status = 200, description = "2FA setup initiated", body = Enable2FAResponse), +// (status = 401, description = "Unauthorized"), +// (status = 409, description = "2FA already enabled"), +// (status = 500, description = "Internal server error"), +// (status = 404, description = "Not found", body = ApiResponse), +// ), +// tag = "Auth" +// )] +// pub async fn api_2fa_enable( +// service: web::Data, +// session: Session, +// ) -> Result { +// let resp = service.auth_2fa_enable(&session).await?; +// Ok(ApiResponse::ok(resp).to_response()) +// } +// +// #[utoipa::path( +// post, +// path = "/api/auth/2fa/verify", +// request_body = Verify2FAParams, +// responses( +// (status = 200, description = "2FA verified and enabled"), +// (status = 401, description = "Unauthorized or invalid code"), +// (status = 400, description = "2FA not set up"), +// (status = 500, description = "Internal server error"), +// (status = 404, description = "Not found", body = ApiResponse), +// ), +// tag = "Auth" +// )] +// pub async fn api_2fa_verify( +// service: web::Data, +// session: Session, +// params: web::Json, +// ) -> Result { +// service +// .auth_2fa_verify_and_enable(&session, params.into_inner()) +// .await?; +// Ok(crate::api_success()) +// } +// +// #[utoipa::path( +// post, +// path = "/api/auth/2fa/disable", +// request_body = Disable2FAParams, +// responses( +// (status = 200, description = "2FA disabled"), +// (status = 401, description = "Unauthorized"), +// (status = 400, description = "2FA not enabled or invalid code/password"), +// (status = 500, description = "Internal server error"), +// (status = 404, description = "Not found", body = ApiResponse), +// ), +// tag = "Auth" +// )] +// pub async fn api_2fa_disable( +// service: web::Data, +// session: Session, +// params: web::Json, +// ) -> Result { +// service +// .auth_2fa_disable(&session, params.into_inner()) +// .await?; +// Ok(crate::api_success()) +// } +// +// #[utoipa::path( +// post, +// path = "/api/auth/2fa/status", +// responses( +// (status = 200, description = "2FA status", body = Get2FAStatusResponse), +// (status = 401, description = "Unauthorized"), +// (status = 500, description = "Internal server error"), +// (status = 404, description = "Not found", body = ApiResponse), +// ), +// tag = "Auth" +// )] +// pub async fn api_2fa_status( +// service: web::Data, +// session: Session, +// ) -> Result { +// let resp = service.auth_2fa_status(&session).await?; +// Ok(ApiResponse::ok(resp).to_response()) +// } +// } diff --git a/libs/api/openapi.rs b/libs/api/openapi.rs index 04b3ffa..54f1d61 100644 --- a/libs/api/openapi.rs +++ b/libs/api/openapi.rs @@ -23,10 +23,10 @@ use utoipa::OpenApi; crate::auth::password::api_user_change_password, crate::auth::password::api_user_request_password_reset, crate::auth::password::api_user_confirm_password_reset, - crate::auth::totp::api_2fa_enable, - crate::auth::totp::api_2fa_verify, - crate::auth::totp::api_2fa_disable, - crate::auth::totp::api_2fa_status, + // crate::auth::totp::api_2fa_enable, + // crate::auth::totp::api_2fa_verify, + // crate::auth::totp::api_2fa_disable, + // crate::auth::totp::api_2fa_status, crate::auth::email::api_email_get, crate::auth::email::api_email_change, crate::auth::email::api_email_verify, @@ -673,10 +673,10 @@ use utoipa::OpenApi; service::auth::captcha::CaptchaQuery, service::auth::captcha::CaptchaResponse, service::auth::me::ContextMe, - service::auth::totp::Enable2FAResponse, - service::auth::totp::Verify2FAParams, - service::auth::totp::Disable2FAParams, - service::auth::totp::Get2FAStatusResponse, + // service::auth::totp::Enable2FAResponse, + // service::auth::totp::Verify2FAParams, + // service::auth::totp::Disable2FAParams, + // service::auth::totp::Get2FAStatusResponse, service::auth::email::EmailChangeRequest, service::auth::email::EmailVerifyRequest, service::auth::email::EmailResponse, diff --git a/libs/service/auth/login.rs b/libs/service/auth/login.rs index d3984ae..699be16 100644 --- a/libs/service/auth/login.rs +++ b/libs/service/auth/login.rs @@ -2,23 +2,23 @@ use crate::AppService; use crate::error::AppError; use argon2::{Argon2, PasswordHash, PasswordVerifier}; use models::users::{user_activity_log, user_password}; -use rand::RngExt; -use redis::AsyncCommands; +// use rand::RngExt; +// use redis::AsyncCommands; use sea_orm::*; use serde::{Deserialize, Serialize}; use session::Session; -use sha1::Digest; +// use sha1::Digest; #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] pub struct LoginParams { pub username: String, pub password: String, pub captcha: String, - pub totp_code: Option, + // pub totp_code: Option, // 2FA disabled } impl AppService { - pub const TOTP_KEY: &'static str = "totp_key"; + // pub const TOTP_KEY: &'static str = "totp_key"; // 2FA disabled pub async fn auth_login(&self, params: LoginParams, context: Session) -> Result<(), AppError> { self.auth_check_captcha(&context, params.captcha).await?; let password = self.auth_rsa_decode(&context, params.password).await?; @@ -47,48 +47,50 @@ impl AppService { return Err(AppError::UserNotFound); } - let needs_totp_verification = context - .get::(Self::TOTP_KEY) - .ok() - .flatten() - .is_some(); - - if needs_totp_verification { - if let Some(ref totp_code) = params.totp_code { - if !self.auth_2fa_verify_login(&context, totp_code).await? { - slog::warn!(self.logs, "Login failed: invalid 2FA code"; "username" => ¶ms.username, "ip" => context.ip_address()); - return Err(AppError::InvalidTwoFactorCode); - } - } - } else if !self.auth_2fa_status_by_uid(user.uid).await?.is_enabled { - let user_uid = user.uid; - let mut rng = rand::rng(); - let mut sha = sha1::Sha1::default(); - for _ in 0..5 { - sha.update( - (0..1024) - .map(|_| { - format!( - "{:04}-{:04}-{:04}", - rng.random_range(0..10000), - rng.random_range(0..10000), - rng.random_range(0..10000) - ) - }) - .collect::() - .as_bytes(), - ) - } - let key = format!("{:?}", sha.finalize()); - context.insert(Self::TOTP_KEY, key.clone()).ok(); - if let Ok(mut conn) = self.cache.conn().await { - conn.set_ex::(key, user_uid.to_string(), 60 * 5) - .await - .ok(); - } - slog::info!(self.logs, "Login 2FA triggered for new 2FA user"; "username" => ¶ms.username, "ip" => context.ip_address()); - return Err(AppError::TwoFactorRequired); - } + // 2FA disabled { + // let needs_totp_verification = context + // .get::(Self::TOTP_KEY) + // .ok() + // .flatten() + // .is_some(); + // + // if needs_totp_verification { + // if let Some(ref totp_code) = params.totp_code { + // if !self.auth_2fa_verify_login(&context, totp_code).await? { + // slog::warn!(self.logs, "Login failed: invalid 2FA code"; "username" => ¶ms.username, "ip" => context.ip_address()); + // return Err(AppError::InvalidTwoFactorCode); + // } + // } + // } else if !self.auth_2fa_status_by_uid(user.uid).await?.is_enabled { + // let user_uid = user.uid; + // let mut rng = rand::rng(); + // let mut sha = sha1::Sha1::default(); + // for _ in 0..5 { + // sha.update( + // (0..1024) + // .map(|_| { + // format!( + // "{:04}-{:04}-{:04}", + // rng.random_range(0..10000), + // rng.random_range(0..10000), + // rng.random_range(0..10000) + // ) + // }) + // .collect::() + // .as_bytes(), + // ) + // } + // let key = format!("{:?}", sha.finalize()); + // context.insert(Self::TOTP_KEY, key.clone()).ok(); + // if let Ok(mut conn) = self.cache.conn().await { + // conn.set_ex::(key, user_uid.to_string(), 60 * 5) + // .await + // .ok(); + // } + // slog::info!(self.logs, "Login 2FA triggered for new 2FA user"; "username" => ¶ms.username, "ip" => context.ip_address()); + // return Err(AppError::TwoFactorRequired); + // } + // } let mut arch = user.clone().into_active_model(); arch.last_sign_in_at = Set(Some(chrono::Utc::now())); @@ -104,7 +106,6 @@ impl AppService { details: Set(Some(serde_json::json!({ "method": "password", "username": user.username, - "2fa_used": params.totp_code.is_some() })) .into()), created_at: Set(chrono::Utc::now()), @@ -116,7 +117,7 @@ impl AppService { context.set_user(user.uid); context.remove(Self::RSA_PRIVATE_KEY); context.remove(Self::RSA_PUBLIC_KEY); - slog::info!(self.logs, "User logged in successfully"; "user_uid" => %user.uid, "username" => &user.username, "ip" => context.ip_address(), "2fa_used" => params.totp_code.is_some()); + slog::info!(self.logs, "User logged in successfully"; "user_uid" => %user.uid, "username" => &user.username, "ip" => context.ip_address()); Ok(()) } } diff --git a/libs/service/auth/mod.rs b/libs/service/auth/mod.rs index 71c4ed2..a772648 100644 --- a/libs/service/auth/mod.rs +++ b/libs/service/auth/mod.rs @@ -6,4 +6,4 @@ pub mod me; pub mod password; pub mod register; pub mod rsa; -pub mod totp; +// pub mod totp; // 2FA disabled diff --git a/src/App.tsx b/src/App.tsx index b0f5070..be28b63 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -105,7 +105,6 @@ function App() { }/> - {/* Landing sub-pages */} }/> }/> }/> diff --git a/src/app/auth/login-page.tsx b/src/app/auth/login-page.tsx index 5792afd..657983b 100644 --- a/src/app/auth/login-page.tsx +++ b/src/app/auth/login-page.tsx @@ -2,7 +2,7 @@ import {useCallback, useEffect, useRef, useState} from "react"; import {Link, useLocation, useNavigate} from "react-router-dom"; import {ArrowRight, Command, Eye, EyeOff, Loader2, ShieldAlert} from "lucide-react"; import {apiAuthCaptcha, type ApiResponseCaptchaResponse} from "@/client"; -import {getApiErrorMessage, isTotpRequiredError} from "@/lib/api-error"; +import {getApiErrorMessage /*, isTotpRequiredError*/} from "@/lib/api-error"; // 2FA disabled import {useUser} from "@/contexts"; import {AuthLayout} from "@/components/auth/auth-layout"; import {Button} from "@/components/ui/button"; @@ -17,10 +17,10 @@ export function LoginPage() { const from = (location.state as { from?: string })?.from || "/w/me"; const usernameRef = useRef(null); - const [form, setForm] = useState({username: "", password: "", captcha: "", totp_code: ""}); + const [form, setForm] = useState({username: "", password: "", captcha: ""}); const [showPassword, setShowPassword] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [needsTotp, setNeedsTotp] = useState(false); + // const [needsTotp, setNeedsTotp] = useState(false); // 2FA disabled const [captcha, setCaptcha] = useState(null); const [captchaLoading, setCaptchaLoading] = useState(false); const [error, setError] = useState(null); @@ -61,17 +61,17 @@ export function LoginPage() { username: form.username, password: encryptedPassword, captcha: form.captcha, - totp_code: needsTotp && form.totp_code ? form.totp_code : undefined, + // totp_code: undefined, // 2FA disabled }); navigate(from, {replace: true}); } catch (err: unknown) { - if (isTotpRequiredError(err)) { - setNeedsTotp(true); - } else { - setError(getApiErrorMessage(err, "Invalid credentials. Please try again.")); - await loadCaptcha(); - setForm((p) => ({...p, captcha: ""})); - } + // if (isTotpRequiredError(err)) { // 2FA disabled + // setNeedsTotp(true); + // } else { + setError(getApiErrorMessage(err, "Invalid credentials. Please try again.")); + await loadCaptcha(); + setForm((p) => ({...p, captcha: ""})); + // } } finally { setIsLoading(false); } @@ -159,7 +159,7 @@ export function LoginPage() { - {/* TOTP */} + {/* 2FA disabled { {needsTotp && ( )} + } */} {/* 优雅的内嵌验证码设计 */} - {!needsTotp && ( -
- -
- setForm((p) => ({...p, captcha: e.target.value}))} - className="h-10 pl-3 pr-[110px] bg-zinc-50 dark:bg-zinc-900/50 border-zinc-200 dark:border-zinc-800 rounded-lg focus-visible:ring-1 focus-visible:ring-zinc-400 dark:focus-visible:ring-zinc-600 transition-shadow" - disabled={isLoading || captchaLoading} - /> - -
+ {/* {needsTotp && (} */} +
+ +
+ setForm((p) => ({...p, captcha: e.target.value}))} + className="h-10 pl-3 pr-[110px] bg-zinc-50 dark:bg-zinc-900/50 border-zinc-200 dark:border-zinc-800 rounded-lg focus-visible:ring-1 focus-visible:ring-zinc-400 dark:focus-visible:ring-zinc-600 transition-shadow" + disabled={isLoading || captchaLoading} + /> +
- )} +
+ {/* )} */} {/* 错误提示 - 使用非常克制的柔和边框风格 */} @@ -265,4 +266,4 @@ export function LoginPage() { ); -} \ No newline at end of file +} diff --git a/src/app/settings/security.tsx b/src/app/settings/security.tsx index 20b498f..dcaa407 100644 --- a/src/app/settings/security.tsx +++ b/src/app/settings/security.tsx @@ -1,10 +1,9 @@ -import { useState } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; export function SettingsSecurity() { - const [twoFactorEnabled, setTwoFactorEnabled] = useState(false); + // const [twoFactorEnabled, setTwoFactorEnabled] = useState(false); // 2FA disabled return (
@@ -22,6 +21,7 @@ export function SettingsSecurity() { + {/* 2FA disabled {

Two-Factor Authentication

@@ -39,6 +39,7 @@ export function SettingsSecurity() {
+ } */}
diff --git a/src/contexts/room-context.tsx b/src/contexts/room-context.tsx index 5e926ec..4244493 100644 --- a/src/contexts/room-context.tsx +++ b/src/contexts/room-context.tsx @@ -439,7 +439,7 @@ export function RoomProvider({ }); } }, - const onAiStreamChunk = useCallback((chunk: AiStreamChunkPayload) => { + onAiStreamChunk: (chunk: { done: boolean; message_id: string; room_id: string; content: string }) => { if (chunk.done) { setStreamingContent((prev) => { prev.delete(chunk.message_id); @@ -491,7 +491,7 @@ export function RoomProvider({ return current; }); } - }, []); + }, onRoomReactionUpdated: (payload: RoomReactionUpdatedPayload) => { if (!activeRoomIdRef.current) return; setMessages((prev) => { diff --git a/src/lib/api-error.ts b/src/lib/api-error.ts index f41e6de..17d77bf 100644 --- a/src/lib/api-error.ts +++ b/src/lib/api-error.ts @@ -6,7 +6,7 @@ export interface ApiErrorBody { message: string; } -export const CODE_TOTP_REQUIRED = 42801; +// export const CODE_TOTP_REQUIRED = 42801; // 2FA disabled export function getApiErrorMessage( error: unknown, @@ -21,13 +21,13 @@ export function getApiErrorMessage( return fallback; } -export function isTotpRequiredError(error: unknown): boolean { - return ( - axios.isAxiosError(error) && - (error.response?.data as Partial | undefined)?.code === - CODE_TOTP_REQUIRED - ); -} +// export function isTotpRequiredError(error: unknown): boolean { // 2FA disabled +// return ( +// axios.isAxiosError(error) && +// (error.response?.data as Partial | undefined)?.code === +// CODE_TOTP_REQUIRED +// ); +// } export function getApiErrorCode(error: unknown): number | undefined { if (axios.isAxiosError(error)) { diff --git a/src/main.tsx b/src/main.tsx index 9b03b83..7a482cb 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,26 +1,25 @@ -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import { BrowserRouter } from 'react-router-dom'; -import { Toaster } from 'sonner'; -import { UserProvider } from '@/contexts'; -import { ThemeProvider } from '@/contexts/theme-context'; +import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; +import {createRoot} from 'react-dom/client'; +import {BrowserRouter} from 'react-router-dom'; +import {Toaster} from 'sonner'; +import {UserProvider} from '@/contexts'; +import {ThemeProvider} from '@/contexts/theme-context'; import './index.css'; import App from './App.tsx'; const queryClient = new QueryClient(); createRoot(document.getElementById('root')!).render( - + <> - - - - - + + + + + - , + , );