"Forgot your password?" on web and mobile sends a one-time link (1 hour) to the account's email; the website's reset page sets the new password, signs out every other session and signs in. The request responds the same whether or not the email has an account, and the email goes out in the background so timing doesn't reveal it either. Only a hash of the link token is stored. Sends through SMTP_* settings; without them, emails are logged (and captured in tests). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
@@ -7,7 +7,7 @@ import type {
|
||||
UserSummaryDTO,
|
||||
} from './dto';
|
||||
import type { DeviceDTO } from './devices';
|
||||
import type { LoginInput, RegisterInput, SetFeaturedInput, UpdateMeInput } from './schemas';
|
||||
import type { LoginInput, RegisterInput, ResetPasswordInput, SetFeaturedInput, UpdateMeInput } from './schemas';
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
@@ -66,6 +66,9 @@ export function createApiClient({ baseUrl, getToken, credentials }: ApiClientOpt
|
||||
|
||||
register: (input: RegisterInput) => request<AuthResponse>('/auth/register', { body: input }),
|
||||
login: (input: LoginInput) => request<AuthResponse>('/auth/login', { body: input }),
|
||||
/** 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 }),
|
||||
logout: () => request('/auth/logout', { method: 'POST', body: {} }),
|
||||
|
||||
me: () => request<MeDTO>('/me'),
|
||||
|
||||
@@ -10,10 +10,19 @@ export const usernameSchema = z
|
||||
// Trim before validating: iOS keyboards often append a space after autocompleting an email.
|
||||
const emailSchema = z.string().trim().toLowerCase().pipe(z.email());
|
||||
|
||||
const newPasswordSchema = z.string().min(8, 'Password must be at least 8 characters').max(200);
|
||||
|
||||
export const registerSchema = z.object({
|
||||
username: usernameSchema,
|
||||
email: emailSchema,
|
||||
password: z.string().min(8, 'Password must be at least 8 characters').max(200),
|
||||
password: newPasswordSchema,
|
||||
});
|
||||
|
||||
export const forgotPasswordSchema = z.object({ email: emailSchema });
|
||||
|
||||
export const resetPasswordSchema = z.object({
|
||||
token: z.string().min(20).max(200),
|
||||
password: newPasswordSchema,
|
||||
});
|
||||
|
||||
export const loginSchema = z.object({
|
||||
@@ -40,6 +49,7 @@ 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 UpdateMeInput = z.infer<typeof updateMeSchema>;
|
||||
export type SetFeaturedInput = z.infer<typeof setFeaturedSchema>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user