feat(room): add category creation and drag-to-assign for channels
Some checks are pending
CI / Rust Lint & Check (push) Waiting to run
CI / Rust Tests (push) Waiting to run
CI / Frontend Lint & Type Check (push) Waiting to run
CI / Frontend Build (push) Blocked by required conditions

- Rewrite DiscordChannelSidebar with @dnd-kit drag-and-drop:
  rooms are sortable within categories; dragging onto a different
  category header assigns the room to that category
- Add inline 'Add Category' button: Enter/Esc to confirm/cancel
- Wire category create/move handlers in room.tsx via RoomContext
- Fix onAiStreamChunk to accumulate content properly and avoid
  redundant re-renders during AI streaming (dedup guard)
- No backend changes needed: category CRUD and room category update
  endpoints were already wired
This commit is contained in:
ZhenYi 2026-04-19 16:44:31 +08:00
parent b73cc8d421
commit 63c75ad453
12 changed files with 235 additions and 241 deletions

3
.gitignore vendored
View File

@ -16,4 +16,5 @@ AGENT.md
ARCHITECTURE.md ARCHITECTURE.md
.agents .agents
.agents.md .agents.md
.next .next
admin

View File

@ -5,7 +5,7 @@ pub mod logout;
pub mod me; pub mod me;
pub mod password; pub mod password;
pub mod register; pub mod register;
pub mod totp; // pub mod totp; // 2FA disabled
pub mod ws_token; pub mod ws_token;
pub fn init_auth_routes(cfg: &mut actix_web::web::ServiceConfig) { 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", "/password/confirm",
actix_web::web::post().to(password::api_user_confirm_password_reset), actix_web::web::post().to(password::api_user_confirm_password_reset),
) )
.route( // 2FA disabled {
"/2fa/enable", // .route("/2fa/enable", actix_web::web::post().to(totp::api_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( // .route("/2fa/status", actix_web::web::post().to(totp::api_2fa_status))
"/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", actix_web::web::post().to(email::api_email_get))
.route( .route(
"/email/change", "/email/change",

View File

@ -1,94 +1,96 @@
use crate::{ApiResponse, error::ApiError}; // 2FA disabled {
use actix_web::{HttpResponse, Result, web}; // use crate::{ApiResponse, error::ApiError};
use service::AppService; // use actix_web::{HttpResponse, Result, web};
use service::auth::totp::{ // use service::AppService;
Disable2FAParams, Enable2FAResponse, Get2FAStatusResponse, Verify2FAParams, // use service::auth::totp::{
}; // Disable2FAParams, Enable2FAResponse, Get2FAStatusResponse, Verify2FAParams,
use session::Session; // };
// use session::Session;
#[utoipa::path( //
post, // #[utoipa::path(
path = "/api/auth/2fa/enable", // post,
responses( // path = "/api/auth/2fa/enable",
(status = 200, description = "2FA setup initiated", body = Enable2FAResponse), // responses(
(status = 401, description = "Unauthorized"), // (status = 200, description = "2FA setup initiated", body = Enable2FAResponse),
(status = 409, description = "2FA already enabled"), // (status = 401, description = "Unauthorized"),
(status = 500, description = "Internal server error"), // (status = 409, description = "2FA already enabled"),
(status = 404, description = "Not found", body = ApiResponse<ApiError>), // (status = 500, description = "Internal server error"),
), // (status = 404, description = "Not found", body = ApiResponse<ApiError>),
tag = "Auth" // ),
)] // tag = "Auth"
pub async fn api_2fa_enable( // )]
service: web::Data<AppService>, // pub async fn api_2fa_enable(
session: Session, // service: web::Data<AppService>,
) -> Result<HttpResponse, ApiError> { // session: Session,
let resp = service.auth_2fa_enable(&session).await?; // ) -> Result<HttpResponse, ApiError> {
Ok(ApiResponse::ok(resp).to_response()) // let resp = service.auth_2fa_enable(&session).await?;
} // Ok(ApiResponse::ok(resp).to_response())
// }
#[utoipa::path( //
post, // #[utoipa::path(
path = "/api/auth/2fa/verify", // post,
request_body = Verify2FAParams, // path = "/api/auth/2fa/verify",
responses( // request_body = Verify2FAParams,
(status = 200, description = "2FA verified and enabled"), // responses(
(status = 401, description = "Unauthorized or invalid code"), // (status = 200, description = "2FA verified and enabled"),
(status = 400, description = "2FA not set up"), // (status = 401, description = "Unauthorized or invalid code"),
(status = 500, description = "Internal server error"), // (status = 400, description = "2FA not set up"),
(status = 404, description = "Not found", body = ApiResponse<ApiError>), // (status = 500, description = "Internal server error"),
), // (status = 404, description = "Not found", body = ApiResponse<ApiError>),
tag = "Auth" // ),
)] // tag = "Auth"
pub async fn api_2fa_verify( // )]
service: web::Data<AppService>, // pub async fn api_2fa_verify(
session: Session, // service: web::Data<AppService>,
params: web::Json<Verify2FAParams>, // session: Session,
) -> Result<HttpResponse, ApiError> { // params: web::Json<Verify2FAParams>,
service // ) -> Result<HttpResponse, ApiError> {
.auth_2fa_verify_and_enable(&session, params.into_inner()) // service
.await?; // .auth_2fa_verify_and_enable(&session, params.into_inner())
Ok(crate::api_success()) // .await?;
} // Ok(crate::api_success())
// }
#[utoipa::path( //
post, // #[utoipa::path(
path = "/api/auth/2fa/disable", // post,
request_body = Disable2FAParams, // path = "/api/auth/2fa/disable",
responses( // request_body = Disable2FAParams,
(status = 200, description = "2FA disabled"), // responses(
(status = 401, description = "Unauthorized"), // (status = 200, description = "2FA disabled"),
(status = 400, description = "2FA not enabled or invalid code/password"), // (status = 401, description = "Unauthorized"),
(status = 500, description = "Internal server error"), // (status = 400, description = "2FA not enabled or invalid code/password"),
(status = 404, description = "Not found", body = ApiResponse<ApiError>), // (status = 500, description = "Internal server error"),
), // (status = 404, description = "Not found", body = ApiResponse<ApiError>),
tag = "Auth" // ),
)] // tag = "Auth"
pub async fn api_2fa_disable( // )]
service: web::Data<AppService>, // pub async fn api_2fa_disable(
session: Session, // service: web::Data<AppService>,
params: web::Json<Disable2FAParams>, // session: Session,
) -> Result<HttpResponse, ApiError> { // params: web::Json<Disable2FAParams>,
service // ) -> Result<HttpResponse, ApiError> {
.auth_2fa_disable(&session, params.into_inner()) // service
.await?; // .auth_2fa_disable(&session, params.into_inner())
Ok(crate::api_success()) // .await?;
} // Ok(crate::api_success())
// }
#[utoipa::path( //
post, // #[utoipa::path(
path = "/api/auth/2fa/status", // post,
responses( // path = "/api/auth/2fa/status",
(status = 200, description = "2FA status", body = Get2FAStatusResponse), // responses(
(status = 401, description = "Unauthorized"), // (status = 200, description = "2FA status", body = Get2FAStatusResponse),
(status = 500, description = "Internal server error"), // (status = 401, description = "Unauthorized"),
(status = 404, description = "Not found", body = ApiResponse<ApiError>), // (status = 500, description = "Internal server error"),
), // (status = 404, description = "Not found", body = ApiResponse<ApiError>),
tag = "Auth" // ),
)] // tag = "Auth"
pub async fn api_2fa_status( // )]
service: web::Data<AppService>, // pub async fn api_2fa_status(
session: Session, // service: web::Data<AppService>,
) -> Result<HttpResponse, ApiError> { // session: Session,
let resp = service.auth_2fa_status(&session).await?; // ) -> Result<HttpResponse, ApiError> {
Ok(ApiResponse::ok(resp).to_response()) // let resp = service.auth_2fa_status(&session).await?;
} // Ok(ApiResponse::ok(resp).to_response())
// }
// }

View File

@ -23,10 +23,10 @@ use utoipa::OpenApi;
crate::auth::password::api_user_change_password, crate::auth::password::api_user_change_password,
crate::auth::password::api_user_request_password_reset, crate::auth::password::api_user_request_password_reset,
crate::auth::password::api_user_confirm_password_reset, crate::auth::password::api_user_confirm_password_reset,
crate::auth::totp::api_2fa_enable, // crate::auth::totp::api_2fa_enable,
crate::auth::totp::api_2fa_verify, // crate::auth::totp::api_2fa_verify,
crate::auth::totp::api_2fa_disable, // crate::auth::totp::api_2fa_disable,
crate::auth::totp::api_2fa_status, // crate::auth::totp::api_2fa_status,
crate::auth::email::api_email_get, crate::auth::email::api_email_get,
crate::auth::email::api_email_change, crate::auth::email::api_email_change,
crate::auth::email::api_email_verify, crate::auth::email::api_email_verify,
@ -673,10 +673,10 @@ use utoipa::OpenApi;
service::auth::captcha::CaptchaQuery, service::auth::captcha::CaptchaQuery,
service::auth::captcha::CaptchaResponse, service::auth::captcha::CaptchaResponse,
service::auth::me::ContextMe, service::auth::me::ContextMe,
service::auth::totp::Enable2FAResponse, // service::auth::totp::Enable2FAResponse,
service::auth::totp::Verify2FAParams, // service::auth::totp::Verify2FAParams,
service::auth::totp::Disable2FAParams, // service::auth::totp::Disable2FAParams,
service::auth::totp::Get2FAStatusResponse, // service::auth::totp::Get2FAStatusResponse,
service::auth::email::EmailChangeRequest, service::auth::email::EmailChangeRequest,
service::auth::email::EmailVerifyRequest, service::auth::email::EmailVerifyRequest,
service::auth::email::EmailResponse, service::auth::email::EmailResponse,

View File

@ -2,23 +2,23 @@ use crate::AppService;
use crate::error::AppError; use crate::error::AppError;
use argon2::{Argon2, PasswordHash, PasswordVerifier}; use argon2::{Argon2, PasswordHash, PasswordVerifier};
use models::users::{user_activity_log, user_password}; use models::users::{user_activity_log, user_password};
use rand::RngExt; // use rand::RngExt;
use redis::AsyncCommands; // use redis::AsyncCommands;
use sea_orm::*; use sea_orm::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use session::Session; use session::Session;
use sha1::Digest; // use sha1::Digest;
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct LoginParams { pub struct LoginParams {
pub username: String, pub username: String,
pub password: String, pub password: String,
pub captcha: String, pub captcha: String,
pub totp_code: Option<String>, // pub totp_code: Option<String>, // 2FA disabled
} }
impl AppService { 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> { pub async fn auth_login(&self, params: LoginParams, context: Session) -> Result<(), AppError> {
self.auth_check_captcha(&context, params.captcha).await?; self.auth_check_captcha(&context, params.captcha).await?;
let password = self.auth_rsa_decode(&context, params.password).await?; let password = self.auth_rsa_decode(&context, params.password).await?;
@ -47,48 +47,50 @@ impl AppService {
return Err(AppError::UserNotFound); return Err(AppError::UserNotFound);
} }
let needs_totp_verification = context // 2FA disabled {
.get::<String>(Self::TOTP_KEY) // let needs_totp_verification = context
.ok() // .get::<String>(Self::TOTP_KEY)
.flatten() // .ok()
.is_some(); // .flatten()
// .is_some();
if needs_totp_verification { //
if let Some(ref totp_code) = params.totp_code { // if needs_totp_verification {
if !self.auth_2fa_verify_login(&context, totp_code).await? { // if let Some(ref totp_code) = params.totp_code {
slog::warn!(self.logs, "Login failed: invalid 2FA code"; "username" => &params.username, "ip" => context.ip_address()); // if !self.auth_2fa_verify_login(&context, totp_code).await? {
return Err(AppError::InvalidTwoFactorCode); // slog::warn!(self.logs, "Login failed: invalid 2FA code"; "username" => &params.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; // } else if !self.auth_2fa_status_by_uid(user.uid).await?.is_enabled {
let mut rng = rand::rng(); // let user_uid = user.uid;
let mut sha = sha1::Sha1::default(); // let mut rng = rand::rng();
for _ in 0..5 { // let mut sha = sha1::Sha1::default();
sha.update( // for _ in 0..5 {
(0..1024) // sha.update(
.map(|_| { // (0..1024)
format!( // .map(|_| {
"{:04}-{:04}-{:04}", // format!(
rng.random_range(0..10000), // "{:04}-{:04}-{:04}",
rng.random_range(0..10000), // rng.random_range(0..10000),
rng.random_range(0..10000) // rng.random_range(0..10000),
) // rng.random_range(0..10000)
}) // )
.collect::<String>() // })
.as_bytes(), // .collect::<String>()
) // .as_bytes(),
} // )
let key = format!("{:?}", sha.finalize()); // }
context.insert(Self::TOTP_KEY, key.clone()).ok(); // let key = format!("{:?}", sha.finalize());
if let Ok(mut conn) = self.cache.conn().await { // context.insert(Self::TOTP_KEY, key.clone()).ok();
conn.set_ex::<String, String, ()>(key, user_uid.to_string(), 60 * 5) // if let Ok(mut conn) = self.cache.conn().await {
.await // conn.set_ex::<String, String, ()>(key, user_uid.to_string(), 60 * 5)
.ok(); // .await
} // .ok();
slog::info!(self.logs, "Login 2FA triggered for new 2FA user"; "username" => &params.username, "ip" => context.ip_address()); // }
return Err(AppError::TwoFactorRequired); // slog::info!(self.logs, "Login 2FA triggered for new 2FA user"; "username" => &params.username, "ip" => context.ip_address());
} // return Err(AppError::TwoFactorRequired);
// }
// }
let mut arch = user.clone().into_active_model(); let mut arch = user.clone().into_active_model();
arch.last_sign_in_at = Set(Some(chrono::Utc::now())); arch.last_sign_in_at = Set(Some(chrono::Utc::now()));
@ -104,7 +106,6 @@ impl AppService {
details: Set(Some(serde_json::json!({ details: Set(Some(serde_json::json!({
"method": "password", "method": "password",
"username": user.username, "username": user.username,
"2fa_used": params.totp_code.is_some()
})) }))
.into()), .into()),
created_at: Set(chrono::Utc::now()), created_at: Set(chrono::Utc::now()),
@ -116,7 +117,7 @@ impl AppService {
context.set_user(user.uid); context.set_user(user.uid);
context.remove(Self::RSA_PRIVATE_KEY); context.remove(Self::RSA_PRIVATE_KEY);
context.remove(Self::RSA_PUBLIC_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(()) Ok(())
} }
} }

View File

@ -6,4 +6,4 @@ pub mod me;
pub mod password; pub mod password;
pub mod register; pub mod register;
pub mod rsa; pub mod rsa;
pub mod totp; // pub mod totp; // 2FA disabled

View File

@ -105,7 +105,6 @@ function App() {
<Route index element={<UserProfile/>}/> <Route index element={<UserProfile/>}/>
</Route> </Route>
{/* Landing sub-pages */}
<Route path="/pricing" element={<PricingPage/>}/> <Route path="/pricing" element={<PricingPage/>}/>
<Route path="/pricing/enterprise" element={<PricingEnterprisePage/>}/> <Route path="/pricing/enterprise" element={<PricingEnterprisePage/>}/>
<Route path="/pricing/faq" element={<PricingFaqPage/>}/> <Route path="/pricing/faq" element={<PricingFaqPage/>}/>

View File

@ -2,7 +2,7 @@ import {useCallback, useEffect, useRef, useState} from "react";
import {Link, useLocation, useNavigate} from "react-router-dom"; import {Link, useLocation, useNavigate} from "react-router-dom";
import {ArrowRight, Command, Eye, EyeOff, Loader2, ShieldAlert} from "lucide-react"; import {ArrowRight, Command, Eye, EyeOff, Loader2, ShieldAlert} from "lucide-react";
import {apiAuthCaptcha, type ApiResponseCaptchaResponse} from "@/client"; 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 {useUser} from "@/contexts";
import {AuthLayout} from "@/components/auth/auth-layout"; import {AuthLayout} from "@/components/auth/auth-layout";
import {Button} from "@/components/ui/button"; import {Button} from "@/components/ui/button";
@ -17,10 +17,10 @@ export function LoginPage() {
const from = (location.state as { from?: string })?.from || "/w/me"; const from = (location.state as { from?: string })?.from || "/w/me";
const usernameRef = useRef<HTMLInputElement>(null); const usernameRef = useRef<HTMLInputElement>(null);
const [form, setForm] = useState({username: "", password: "", captcha: "", totp_code: ""}); const [form, setForm] = useState({username: "", password: "", captcha: ""});
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [needsTotp, setNeedsTotp] = useState(false); // const [needsTotp, setNeedsTotp] = useState(false); // 2FA disabled
const [captcha, setCaptcha] = useState<ApiResponseCaptchaResponse['data'] | null>(null); const [captcha, setCaptcha] = useState<ApiResponseCaptchaResponse['data'] | null>(null);
const [captchaLoading, setCaptchaLoading] = useState(false); const [captchaLoading, setCaptchaLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -61,17 +61,17 @@ export function LoginPage() {
username: form.username, username: form.username,
password: encryptedPassword, password: encryptedPassword,
captcha: form.captcha, captcha: form.captcha,
totp_code: needsTotp && form.totp_code ? form.totp_code : undefined, // totp_code: undefined, // 2FA disabled
}); });
navigate(from, {replace: true}); navigate(from, {replace: true});
} catch (err: unknown) { } catch (err: unknown) {
if (isTotpRequiredError(err)) { // if (isTotpRequiredError(err)) { // 2FA disabled
setNeedsTotp(true); // setNeedsTotp(true);
} else { // } else {
setError(getApiErrorMessage(err, "Invalid credentials. Please try again.")); setError(getApiErrorMessage(err, "Invalid credentials. Please try again."));
await loadCaptcha(); await loadCaptcha();
setForm((p) => ({...p, captcha: ""})); setForm((p) => ({...p, captcha: ""}));
} // }
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -159,7 +159,7 @@ export function LoginPage() {
</div> </div>
</div> </div>
{/* TOTP */} {/* 2FA disabled {
<AnimatePresence> <AnimatePresence>
{needsTotp && ( {needsTotp && (
<motion.div <motion.div
@ -186,40 +186,41 @@ export function LoginPage() {
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
} */}
{/* 优雅的内嵌验证码设计 */} {/* 优雅的内嵌验证码设计 */}
{!needsTotp && ( {/* {needsTotp && (} */}
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="captcha" <label htmlFor="captcha"
className="text-sm font-medium text-zinc-900 dark:text-zinc-200"> className="text-sm font-medium text-zinc-900 dark:text-zinc-200">
Verification Verification
</label> </label>
<div className="relative flex items-center"> <div className="relative flex items-center">
<Input <Input
id="captcha" id="captcha"
placeholder="Enter code" placeholder="Enter code"
value={form.captcha} value={form.captcha}
onChange={(e) => setForm((p) => ({...p, captcha: e.target.value}))} onChange={(e) => 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" 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} disabled={isLoading || captchaLoading}
/> />
<button <button
type="button" type="button"
onClick={loadCaptcha} onClick={loadCaptcha}
disabled={captchaLoading} disabled={captchaLoading}
className="absolute right-1 top-1 bottom-1 w-[100px] rounded-md overflow-hidden bg-white dark:bg-zinc-800 border border-zinc-100 dark:border-zinc-700 hover:opacity-80 transition-opacity flex items-center justify-center cursor-pointer" className="absolute right-1 top-1 bottom-1 w-[100px] rounded-md overflow-hidden bg-white dark:bg-zinc-800 border border-zinc-100 dark:border-zinc-700 hover:opacity-80 transition-opacity flex items-center justify-center cursor-pointer"
> >
{captchaLoading ? ( {captchaLoading ? (
<Loader2 className="size-4 animate-spin text-zinc-400"/> <Loader2 className="size-4 animate-spin text-zinc-400"/>
) : captcha ? ( ) : captcha ? (
// 暗黑模式下直接通过滤镜反转图片颜色,极其优雅 // 暗黑模式下直接通过滤镜反转图片颜色,极其优雅
<img src={captcha.base64} alt="captcha" <img src={captcha.base64} alt="captcha"
className="h-full w-full object-cover dark:invert dark:hue-rotate-180"/> className="h-full w-full object-cover dark:invert dark:hue-rotate-180"/>
) : null} ) : null}
</button> </button>
</div>
</div> </div>
)} </div>
{/* )} */}
{/* 错误提示 - 使用非常克制的柔和边框风格 */} {/* 错误提示 - 使用非常克制的柔和边框风格 */}
<AnimatePresence> <AnimatePresence>
@ -265,4 +266,4 @@ export function LoginPage() {
</motion.div> </motion.div>
</AuthLayout> </AuthLayout>
); );
} }

View File

@ -1,10 +1,9 @@
import { useState } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
export function SettingsSecurity() { export function SettingsSecurity() {
const [twoFactorEnabled, setTwoFactorEnabled] = useState(false); // const [twoFactorEnabled, setTwoFactorEnabled] = useState(false); // 2FA disabled
return ( return (
<div className="flex-1 lg:max-w-4xl space-y-6 p-4 md:p-8 pt-6"> <div className="flex-1 lg:max-w-4xl space-y-6 p-4 md:p-8 pt-6">
@ -22,6 +21,7 @@ export function SettingsSecurity() {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{/* 2FA disabled {
<div className="flex flex-col md:flex-row md:items-center justify-between p-4 rounded-lg border bg-card hover:bg-accent/5 transition-colors"> <div className="flex flex-col md:flex-row md:items-center justify-between p-4 rounded-lg border bg-card hover:bg-accent/5 transition-colors">
<div className="space-y-1"> <div className="space-y-1">
<h3 className="font-medium">Two-Factor Authentication</h3> <h3 className="font-medium">Two-Factor Authentication</h3>
@ -39,6 +39,7 @@ export function SettingsSecurity() {
</Button> </Button>
</div> </div>
</div> </div>
} */}
<div className="flex flex-col md:flex-row md:items-center justify-between p-4 rounded-lg border bg-card hover:bg-accent/5 transition-colors"> <div className="flex flex-col md:flex-row md:items-center justify-between p-4 rounded-lg border bg-card hover:bg-accent/5 transition-colors">
<div className="space-y-1"> <div className="space-y-1">

View File

@ -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) { if (chunk.done) {
setStreamingContent((prev) => { setStreamingContent((prev) => {
prev.delete(chunk.message_id); prev.delete(chunk.message_id);
@ -491,7 +491,7 @@ export function RoomProvider({
return current; return current;
}); });
} }
}, []); },
onRoomReactionUpdated: (payload: RoomReactionUpdatedPayload) => { onRoomReactionUpdated: (payload: RoomReactionUpdatedPayload) => {
if (!activeRoomIdRef.current) return; if (!activeRoomIdRef.current) return;
setMessages((prev) => { setMessages((prev) => {

View File

@ -6,7 +6,7 @@ export interface ApiErrorBody {
message: string; message: string;
} }
export const CODE_TOTP_REQUIRED = 42801; // export const CODE_TOTP_REQUIRED = 42801; // 2FA disabled
export function getApiErrorMessage( export function getApiErrorMessage(
error: unknown, error: unknown,
@ -21,13 +21,13 @@ export function getApiErrorMessage(
return fallback; return fallback;
} }
export function isTotpRequiredError(error: unknown): boolean { // export function isTotpRequiredError(error: unknown): boolean { // 2FA disabled
return ( // return (
axios.isAxiosError(error) && // axios.isAxiosError(error) &&
(error.response?.data as Partial<ApiErrorBody> | undefined)?.code === // (error.response?.data as Partial<ApiErrorBody> | undefined)?.code ===
CODE_TOTP_REQUIRED // CODE_TOTP_REQUIRED
); // );
} // }
export function getApiErrorCode(error: unknown): number | undefined { export function getApiErrorCode(error: unknown): number | undefined {
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {

View File

@ -1,26 +1,25 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
import { StrictMode } from 'react'; import {createRoot} from 'react-dom/client';
import { createRoot } from 'react-dom/client'; import {BrowserRouter} from 'react-router-dom';
import { BrowserRouter } from 'react-router-dom'; import {Toaster} from 'sonner';
import { Toaster } from 'sonner'; import {UserProvider} from '@/contexts';
import { UserProvider } from '@/contexts'; import {ThemeProvider} from '@/contexts/theme-context';
import { ThemeProvider } from '@/contexts/theme-context';
import './index.css'; import './index.css';
import App from './App.tsx'; import App from './App.tsx';
const queryClient = new QueryClient(); const queryClient = new QueryClient();
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<BrowserRouter> <BrowserRouter>
<UserProvider> <UserProvider>
<ThemeProvider> <ThemeProvider>
<App /> <App/>
<Toaster richColors position="bottom-right" /> <Toaster richColors position="bottom-right"/>
</ThemeProvider> </ThemeProvider>
</UserProvider> </UserProvider>
</BrowserRouter> </BrowserRouter>
</QueryClientProvider> </QueryClientProvider>
</StrictMode>, </>,
); );