mirror of
https://github.com/temetro/temetro.git
synced 2026-08-24 08:46:27 +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:
@@ -9,6 +9,7 @@ import { env } from "./env.js";
|
||||
import { errorHandler, notFound } from "./middleware/error.js";
|
||||
import { initRealtime } from "./realtime.js";
|
||||
import { activityRouter } from "./routes/activity.js";
|
||||
import { authHelpersRouter } from "./routes/auth-helpers.js";
|
||||
import { aiRouter } from "./routes/ai.js";
|
||||
import { analyticsRouter } from "./routes/analytics.js";
|
||||
import { attachmentsRouter } from "./routes/attachments.js";
|
||||
@@ -86,6 +87,7 @@ app.use("/api/ai", aiRouter);
|
||||
app.use("/api/chat", chatRouter);
|
||||
app.use("/api/integrations", integrationsRouter);
|
||||
app.use("/api/portal", portalRouter);
|
||||
app.use("/api/auth-helpers", authHelpersRouter);
|
||||
|
||||
app.use(notFound);
|
||||
app.use(errorHandler);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { Router } from "express";
|
||||
import { z } from "zod";
|
||||
|
||||
import { auth } from "../auth.js";
|
||||
import { db } from "../db/index.js";
|
||||
import { user } from "../db/schema/auth.js";
|
||||
|
||||
// Public auth helpers that sit alongside Better Auth's own /api/auth handler.
|
||||
//
|
||||
// Better Auth's password-reset endpoint is keyed by email, but staff
|
||||
// provisioned by an admin sign in with a *username* (and may only have a
|
||||
// synthetic `username@slug.temetro.local` address). This lets them start a reset
|
||||
// by username: we resolve the username to its account, then hand off to the
|
||||
// normal reset flow (which emails a link if a provider is configured, or alerts
|
||||
// the clinic admins otherwise — see src/auth.ts sendResetPassword).
|
||||
export const authHelpersRouter = Router();
|
||||
|
||||
const resetByUsernameSchema = z.object({
|
||||
username: z.string().trim().min(1).max(64),
|
||||
redirectTo: z.string().trim().max(2048).optional(),
|
||||
});
|
||||
|
||||
// POST /api/auth-helpers/reset-by-username
|
||||
// Always responds 200 with a generic body — never reveals whether the username
|
||||
// exists (avoids account enumeration) and never echoes the resolved email.
|
||||
authHelpersRouter.post("/reset-by-username", async (req, res, next) => {
|
||||
try {
|
||||
const { username, redirectTo } = resetByUsernameSchema.parse(req.body);
|
||||
|
||||
const [account] = await db
|
||||
.select({ email: user.email })
|
||||
.from(user)
|
||||
.where(eq(user.username, username.toLowerCase()))
|
||||
.limit(1);
|
||||
|
||||
if (account?.email) {
|
||||
// Reuse Better Auth's reset flow so the same dispatch/fallback logic runs.
|
||||
await auth.api.requestPasswordReset({
|
||||
body: { email: account.email, redirectTo },
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
@@ -145,10 +145,14 @@ async function buildSummaries(
|
||||
: (others[0]?.name ?? "Conversation"));
|
||||
const lastMessage = lastByConv.get(b.convId) ?? null;
|
||||
const unreadCount = unreadByConv.get(b.convId) ?? 0;
|
||||
// A one-way System notice (e.g. forgot-password alerts): the reserved system
|
||||
// user is a participant. The UI hides the composer/call and styles it apart.
|
||||
const isSystem = participants.some((p) => p.id === SYSTEM_USER_ID);
|
||||
return {
|
||||
id: b.convId,
|
||||
name: displayName,
|
||||
isGroup: b.isGroup,
|
||||
isSystem,
|
||||
participants,
|
||||
lastMessage,
|
||||
unread: unreadCount > 0,
|
||||
|
||||
@@ -50,6 +50,7 @@ export type ConversationSummary = {
|
||||
id: string;
|
||||
name: string; // display name: group name, or the other participant for a DM
|
||||
isGroup: boolean;
|
||||
isSystem: boolean; // a one-way System notice (no replies / calls)
|
||||
participants: Participant[];
|
||||
lastMessage: ConversationMessage | null;
|
||||
unread: boolean;
|
||||
|
||||
Reference in New Issue
Block a user