feat: username reset, system-message UX, meetings polish, sidebar identity

- auth: forgot-password now has Email/Username tabs; new public
  /api/auth-helpers/reset-by-username resolves a username and hands off to the
  existing reset flow (real email or admin-notify fallback)
- messages: conversations expose isSystem; System notices are read-only (no
  composer, no call button) and styled distinctly (shield icon + badge)
- meetings: compact, larger control bar; self tile shows real initials and an
  avatar (not black) when the camera is off; delete-room UI with confirm
- sidebar: username-only accounts show @username instead of a synthetic email

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-20 22:39:48 +03:00
parent 90e6ec4cc0
commit b74d5d2d05
12 changed files with 343 additions and 97 deletions
+71 -19
View File
@@ -7,12 +7,19 @@ import { useTranslation } from "react-i18next";
import { AuthShell, Field, FormAlert } from "@/components/auth/auth-ui";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Tabs, TabsList, TabsTab } from "@/components/ui/tabs";
import { authClient } from "@/lib/auth-client";
import { requestResetByUsername } from "@/lib/auth-helpers";
import { notify } from "@/lib/toast";
type Mode = "email" | "username";
export default function ForgotPasswordPage() {
const { t } = useTranslation();
const [email, setEmail] = useState("");
// Owners reset by the email they signed up with; admin-provisioned staff reset
// by their username (their email may be a synthetic placeholder).
const [mode, setMode] = useState<Mode>("email");
const [identifier, setIdentifier] = useState("");
const [sent, setSent] = useState(false);
const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
@@ -23,18 +30,33 @@ export default function ForgotPasswordPage() {
setSubmitting(true);
setError(null);
const { error: err } = await authClient.requestPasswordReset({
email: email.trim(),
redirectTo: `${window.location.origin}/reset-password`,
});
const redirectTo = `${window.location.origin}/reset-password`;
setSubmitting(false);
if (err) {
const message = err.message ?? t("auth.forgotPassword.error");
setError(message);
notify.error(t("auth.forgotPassword.failedToastTitle"), message);
return;
if (mode === "email") {
const { error: err } = await authClient.requestPasswordReset({
email: identifier.trim(),
redirectTo,
});
setSubmitting(false);
if (err) {
const message = err.message ?? t("auth.forgotPassword.error");
setError(message);
notify.error(t("auth.forgotPassword.failedToastTitle"), message);
return;
}
} else {
try {
await requestResetByUsername(identifier.trim(), redirectTo);
} catch {
setSubmitting(false);
const message = t("auth.forgotPassword.error");
setError(message);
notify.error(t("auth.forgotPassword.failedToastTitle"), message);
return;
}
setSubmitting(false);
}
notify.success(
t("auth.forgotPassword.sentToastTitle"),
t("auth.forgotPassword.sentToastBody"),
@@ -54,20 +76,50 @@ export default function ForgotPasswordPage() {
>
{sent ? (
<FormAlert tone="success">
{t("auth.forgotPassword.sent", { email })}
{mode === "email"
? t("auth.forgotPassword.sent", { email: identifier })
: t("auth.forgotPassword.sentUsername")}
</FormAlert>
) : (
<form className="flex flex-col gap-4" onSubmit={onSubmit}>
{error && <FormAlert>{error}</FormAlert>}
<Field htmlFor="email" label={t("auth.forgotPassword.emailLabel")}>
<Tabs
onValueChange={(value) => {
setMode(value as Mode);
setError(null);
setIdentifier("");
}}
value={mode}
>
<TabsList className="w-full">
<TabsTab className="flex-1" value="email">
{t("auth.forgotPassword.modeEmail")}
</TabsTab>
<TabsTab className="flex-1" value="username">
{t("auth.forgotPassword.modeUsername")}
</TabsTab>
</TabsList>
</Tabs>
<Field
htmlFor="identifier"
label={
mode === "email"
? t("auth.forgotPassword.emailLabel")
: t("auth.forgotPassword.usernameLabel")
}
>
<Input
autoComplete="email"
id="email"
onChange={(e) => setEmail(e.target.value)}
placeholder={t("auth.forgotPassword.emailPlaceholder")}
autoComplete={mode === "email" ? "email" : "username"}
id="identifier"
onChange={(e) => setIdentifier(e.target.value)}
placeholder={
mode === "email"
? t("auth.forgotPassword.emailPlaceholder")
: t("auth.forgotPassword.usernamePlaceholder")
}
required
type="email"
value={email}
type={mode === "email" ? "email" : "text"}
value={identifier}
/>
</Field>
<Button className="mt-1 w-full" disabled={submitting} type="submit">