feat(F-099): completed feature
This commit is contained in:
@@ -39,6 +39,7 @@ import {
|
||||
RequestPasswordReset,
|
||||
} from '../application/password-reset.js';
|
||||
import { InvalidResetTokenError } from '../domain/password-reset.js';
|
||||
import { createPasswordResetMailer } from '../infrastructure/smtp-password-reset-mailer.js';
|
||||
|
||||
export const SESSION_COOKIE_NAME = 'mdv_session';
|
||||
|
||||
@@ -234,15 +235,30 @@ export async function registerIdentityRoutes(
|
||||
}
|
||||
});
|
||||
|
||||
app.patch('/auth/me/password', async (request, reply) => {
|
||||
const user = await deps.authenticate!(request);
|
||||
const input = parseJson(
|
||||
z.object({ currentPassword: z.string().min(1).max(128), newPassword: z.string().min(8).max(128) }),
|
||||
request.body,
|
||||
);
|
||||
const record = await users.findByEmail(user.email);
|
||||
if (!record || !(await hasher.verify(record.passwordHash, input.currentPassword))) {
|
||||
throw new AppError(400, 'INVALID_CURRENT_PASSWORD', 'La contraseña actual no es válida');
|
||||
}
|
||||
await users.updateUser(user.id, { passwordHash: await hasher.hash(input.newPassword) });
|
||||
return reply.send({ ok: true });
|
||||
});
|
||||
|
||||
if (deps.passwordReset) {
|
||||
const pr = deps.passwordReset;
|
||||
const tokens = pr.tokens ?? new PgPasswordResetTokenRepository(deps.pool);
|
||||
const usersRepo = new PgUserRepository(deps.pool);
|
||||
const rateLimiter = pr.rateLimiter ?? new InMemoryResetRateLimiter();
|
||||
const mailer = pr.mailer ?? new LoggingPasswordResetMailer();
|
||||
const mailer = pr.mailer ?? createPasswordResetMailer();
|
||||
const publicAppUrl = (process.env.PUBLIC_APP_URL ?? 'https://mercadodevida.es').replace(/\/$/, '');
|
||||
const buildResetUrl =
|
||||
pr.buildResetUrl ??
|
||||
((token: string) => `/cuenta/restablecer?token=${encodeURIComponent(token)}`);
|
||||
((token: string) => `${publicAppUrl}/cuenta/restablecer?token=${encodeURIComponent(token)}`);
|
||||
|
||||
const requestReset = new RequestPasswordReset({
|
||||
users: usersRepo,
|
||||
@@ -293,6 +309,15 @@ export async function registerIdentityRoutes(
|
||||
'/auth/password-reset/request',
|
||||
{ schema: requestSchema },
|
||||
async (request, reply) => {
|
||||
if (mailer.assertReady) {
|
||||
try {
|
||||
await mailer.assertReady();
|
||||
} catch {
|
||||
throw new AppError(422, 'EMAIL_DELIVERY_NOT_CONFIGURED', 'Configura SMTP en Ajustes → SMTP / Email');
|
||||
}
|
||||
} else if (mailer.isConfigured && !mailer.isConfigured()) {
|
||||
throw new AppError(422, 'EMAIL_DELIVERY_NOT_CONFIGURED', 'Configura SMTP en Ajustes → SMTP / Email');
|
||||
}
|
||||
const input = parseJson(
|
||||
z.object({ email: z.email().max(255) }),
|
||||
request.body,
|
||||
@@ -350,22 +375,6 @@ export class InMemoryResetRateLimiter implements ResetRateLimiter {
|
||||
}
|
||||
}
|
||||
|
||||
/** Logs the reset email to stdout; production should replace with a real provider. */
|
||||
export class LoggingPasswordResetMailer implements PasswordResetMailer {
|
||||
async sendPasswordReset(input: { email: string; resetUrl: string; locale?: string }): Promise<void> {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
level: 'info',
|
||||
msg: 'password_reset_email',
|
||||
to: input.email,
|
||||
url: input.resetUrl,
|
||||
locale: input.locale ?? 'es',
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function setSessionCookie(reply: FastifyReply, token: string, secure: boolean): void {
|
||||
void reply.setCookie(SESSION_COOKIE_NAME, token, {
|
||||
path: '/',
|
||||
|
||||
Reference in New Issue
Block a user