-
Notifications
You must be signed in to change notification settings - Fork 121
/
ForgotPasswordHandler.ts
86 lines (77 loc) · 3.33 KB
/
ForgotPasswordHandler.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import assert from 'assert';
import { BasicRepresentation } from '../../../../http/representation/BasicRepresentation';
import type { Representation } from '../../../../http/representation/Representation';
import { getLoggerFor } from '../../../../logging/LogUtil';
import { APPLICATION_JSON } from '../../../../util/ContentTypes';
import { readJsonStream } from '../../../../util/StreamUtil';
import type { TemplateEngine } from '../../../../util/templates/TemplateEngine';
import { BaseInteractionHandler } from '../../BaseInteractionHandler';
import type { InteractionHandlerInput } from '../../InteractionHandler';
import type { InteractionRoute } from '../../routing/InteractionRoute';
import type { AccountStore } from '../storage/AccountStore';
import type { EmailSender } from '../util/EmailSender';
const forgotPasswordView = {
required: {
email: 'string',
},
} as const;
export interface ForgotPasswordHandlerArgs {
accountStore: AccountStore;
templateEngine: TemplateEngine<{ resetLink: string }>;
emailSender: EmailSender;
resetRoute: InteractionRoute;
}
/**
* Handles the submission of the ForgotPassword form
*/
export class ForgotPasswordHandler extends BaseInteractionHandler {
protected readonly logger = getLoggerFor(this);
private readonly accountStore: AccountStore;
private readonly templateEngine: TemplateEngine<{ resetLink: string }>;
private readonly emailSender: EmailSender;
private readonly resetRoute: InteractionRoute;
public constructor(args: ForgotPasswordHandlerArgs) {
super(forgotPasswordView);
this.accountStore = args.accountStore;
this.templateEngine = args.templateEngine;
this.emailSender = args.emailSender;
this.resetRoute = args.resetRoute;
}
public async handlePost({ operation }: InteractionHandlerInput): Promise<Representation> {
// Validate incoming data
const { email } = await readJsonStream(operation.body.data);
assert(typeof email === 'string' && email.length > 0, 'Email required');
await this.resetPassword(email);
return new BasicRepresentation(JSON.stringify({ email }), operation.target, APPLICATION_JSON);
}
/**
* Generates a record to reset the password for the given email address and then mails it.
* In case there is no account, no error wil be thrown for privacy reasons.
* Instead nothing will happen instead.
*/
private async resetPassword(email: string): Promise<void> {
let recordId: string;
try {
recordId = await this.accountStore.generateForgotPasswordRecord(email);
} catch {
// Don't emit an error for privacy reasons
this.logger.warn(`Password reset request for unknown email ${email}`);
return;
}
await this.sendResetMail(recordId, email);
}
/**
* Generates the link necessary for resetting the password and mails it to the given email address.
*/
private async sendResetMail(recordId: string, email: string): Promise<void> {
this.logger.info(`Sending password reset to ${email}`);
const resetLink = `${this.resetRoute.getPath()}?rid=${encodeURIComponent(recordId)}`;
const renderedEmail = await this.templateEngine.handleSafe({ contents: { resetLink }});
await this.emailSender.handleSafe({
recipient: email,
subject: 'Reset your password',
text: `To reset your password, go to this link: ${resetLink}`,
html: renderedEmail,
});
}
}