Email confirmation, change email/password, show/hide passwords
CI / check (push) Canceled after 0s

- Sign-up emails a confirmation link (24h); unconfirmed accounts see a
  banner on the web with "send it again", and status in Settings.
- Change email (needs the password): the new address gets a confirmation
  link and only replaces the old one once clicked; the old address gets a
  heads-up with a reset-password link. Pending changes can be cancelled.
- Change password (needs the current one): signs out other sessions.
- Password fields have Show/Hide on web and mobile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
2026-10-06 13:05:26 -04:00
co-authored by Claude Opus 5.5
parent 45a7bd190a
commit 85e1643c29
25 changed files with 2077 additions and 19 deletions
+17 -1
View File
@@ -10,7 +10,16 @@ import type {
UserSummaryDTO,
} from './dto';
import type { DeviceDTO } from './devices';
import type { LoginInput, RegisterInput, ReportReason, ResetPasswordInput, SetFeaturedInput, UpdateMeInput } from './schemas';
import type {
ChangeEmailInput,
ChangePasswordInput,
LoginInput,
RegisterInput,
ReportReason,
ResetPasswordInput,
SetFeaturedInput,
UpdateMeInput,
} from './schemas';
export class ApiError extends Error {
constructor(
@@ -72,6 +81,13 @@ export function createApiClient({ baseUrl, getToken, credentials }: ApiClientOpt
/** Always succeeds, so it doesn't reveal which emails have accounts. */
forgotPassword: (email: string) => request<{ ok: true }>('/auth/forgot-password', { body: { email } }),
resetPassword: (input: ResetPasswordInput) => request<AuthResponse>('/auth/reset-password', { body: input }),
verifyEmail: (token: string) => request<{ ok: true; email: string }>('/auth/verify-email', { body: { token } }),
resendVerification: () => request<{ ok: true }>('/me/verify-email/resend', { body: {} }),
/** Signs out your other devices. */
changePassword: (input: ChangePasswordInput) => request<{ ok: true }>('/me/password', { body: input }),
/** Sends a confirmation link to the new address; the email changes once it's clicked. */
changeEmail: (input: ChangeEmailInput) => request<MeDTO>('/me/email', { body: input }),
cancelEmailChange: () => request<MeDTO>('/me/email/pending', { method: 'DELETE' }),
logout: () => request('/auth/logout', { method: 'POST', body: {} }),
me: () => request<MeDTO>('/me'),
+3
View File
@@ -21,6 +21,9 @@ export interface UserSummaryDTO {
export interface MeDTO extends UserSummaryDTO {
email: string;
emailVerified: boolean;
/** A new address waiting to be confirmed (the link was sent there). */
pendingEmail: string | null;
bio: string | null;
preferredService: StreamingService | null;
role: 'user' | 'admin';
+14
View File
@@ -20,6 +20,18 @@ export const registerSchema = z.object({
export const forgotPasswordSchema = z.object({ email: emailSchema });
export const verifyEmailSchema = z.object({ token: z.string().min(20).max(200) });
export const changePasswordSchema = z.object({
currentPassword: z.string().min(1).max(200),
newPassword: newPasswordSchema,
});
export const changeEmailSchema = z.object({
email: emailSchema,
password: z.string().min(1).max(200),
});
export const resetPasswordSchema = z.object({
token: z.string().min(20).max(200),
password: newPasswordSchema,
@@ -50,6 +62,8 @@ export const connectUsernameSchema = z.object({
export type RegisterInput = z.infer<typeof registerSchema>;
export type LoginInput = z.infer<typeof loginSchema>;
export type ResetPasswordInput = z.infer<typeof resetPasswordSchema>;
export type ChangePasswordInput = z.infer<typeof changePasswordSchema>;
export type ChangeEmailInput = z.infer<typeof changeEmailSchema>;
export type UpdateMeInput = z.infer<typeof updateMeSchema>;
export type SetFeaturedInput = z.infer<typeof setFeaturedSchema>;