mirror of
https://github.com/temetro/temetro.git
synced 2026-08-31 04:47:57 +00:00
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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user