@xcelsior/auth 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +22 -0
- package/.turbo/turbo-lint.log +5 -0
- package/.turbo/turbo-test.log +12 -0
- package/CHANGELOG.md +7 -0
- package/README.md +213 -0
- package/biome.json +3 -0
- package/dist/index.d.mts +139 -0
- package/dist/index.d.ts +139 -0
- package/dist/index.js +532 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +499 -0
- package/dist/index.mjs.map +1 -0
- package/jest.config.js +10 -0
- package/package.json +40 -0
- package/src/email/defaultTemplates.ts +57 -0
- package/src/email/index.ts +22 -0
- package/src/email/ses.ts +80 -0
- package/src/email/smtp.ts +43 -0
- package/src/email/types.ts +42 -0
- package/src/index.ts +3 -0
- package/src/middleware/auth.ts +50 -0
- package/src/services/auth.ts +165 -0
- package/src/storage/dynamodb.ts +153 -0
- package/src/storage/index.ts +18 -0
- package/src/storage/types.ts +33 -0
- package/src/types/index.ts +32 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +10 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
// src/services/auth.ts
|
|
2
|
+
import bcrypt from "bcryptjs";
|
|
3
|
+
import jwt from "jsonwebtoken";
|
|
4
|
+
import { v4 as uuidv4 } from "uuid";
|
|
5
|
+
|
|
6
|
+
// src/storage/dynamodb.ts
|
|
7
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
8
|
+
import {
|
|
9
|
+
DynamoDBDocumentClient,
|
|
10
|
+
PutCommand,
|
|
11
|
+
GetCommand,
|
|
12
|
+
UpdateCommand,
|
|
13
|
+
DeleteCommand,
|
|
14
|
+
QueryCommand
|
|
15
|
+
} from "@aws-sdk/lib-dynamodb";
|
|
16
|
+
var DynamoDBStorageProvider = class {
|
|
17
|
+
constructor(config) {
|
|
18
|
+
const dbClient = new DynamoDBClient({ region: config.region });
|
|
19
|
+
this.client = DynamoDBDocumentClient.from(dbClient, {});
|
|
20
|
+
this.tableName = config.tableName;
|
|
21
|
+
}
|
|
22
|
+
async createUser(user) {
|
|
23
|
+
await this.client.send(
|
|
24
|
+
new PutCommand({
|
|
25
|
+
TableName: this.tableName,
|
|
26
|
+
Item: user,
|
|
27
|
+
ConditionExpression: "attribute_not_exists(email)"
|
|
28
|
+
})
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
async getUserByResetPasswordToken(resetPasswordToken) {
|
|
32
|
+
const response = await this.client.send(
|
|
33
|
+
new QueryCommand({
|
|
34
|
+
TableName: this.tableName,
|
|
35
|
+
IndexName: "ResetPasswordTokenIndex",
|
|
36
|
+
KeyConditionExpression: "resetPasswordToken = :token",
|
|
37
|
+
ExpressionAttributeValues: {
|
|
38
|
+
":token": resetPasswordToken
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
return response.Items?.[0];
|
|
43
|
+
}
|
|
44
|
+
async getUserByVerifyEmailToken(verifyEmailToken) {
|
|
45
|
+
const response = await this.client.send(
|
|
46
|
+
new QueryCommand({
|
|
47
|
+
TableName: this.tableName,
|
|
48
|
+
IndexName: "VerifyEmailTokenIndex",
|
|
49
|
+
KeyConditionExpression: "verificationToken = :token",
|
|
50
|
+
ExpressionAttributeValues: {
|
|
51
|
+
":token": verifyEmailToken
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
return response.Items?.[0];
|
|
56
|
+
}
|
|
57
|
+
async getUserById(id) {
|
|
58
|
+
const response = await this.client.send(
|
|
59
|
+
new GetCommand({
|
|
60
|
+
TableName: this.tableName,
|
|
61
|
+
Key: { id }
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
return response.Item;
|
|
65
|
+
}
|
|
66
|
+
async getUserByEmail(email) {
|
|
67
|
+
const response = await this.client.send(
|
|
68
|
+
new QueryCommand({
|
|
69
|
+
TableName: this.tableName,
|
|
70
|
+
IndexName: "EmailIndex",
|
|
71
|
+
KeyConditionExpression: "email = :email",
|
|
72
|
+
ExpressionAttributeValues: {
|
|
73
|
+
":email": email
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
return response.Items?.[0];
|
|
78
|
+
}
|
|
79
|
+
async updateUser(id, updates) {
|
|
80
|
+
const toSet = {};
|
|
81
|
+
const toRemove = [];
|
|
82
|
+
Object.entries(updates).forEach(([key, value]) => {
|
|
83
|
+
if (value === void 0) {
|
|
84
|
+
toRemove.push(key);
|
|
85
|
+
} else {
|
|
86
|
+
toSet[key] = value;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
if (Object.keys(toSet).length === 0 && toRemove.length === 0) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const setPart = Object.keys(toSet).length > 0 ? `SET ${Object.keys(toSet).map((key) => `#${key} = :${key}`).join(", ")}` : "";
|
|
93
|
+
const removePart = toRemove.length > 0 ? `REMOVE ${toRemove.map((key) => `#${key}`).join(", ")}` : "";
|
|
94
|
+
const updateExpression = [setPart, removePart].filter(Boolean).join(" ");
|
|
95
|
+
const expressionAttributeNames = [...Object.keys(toSet), ...toRemove].reduce(
|
|
96
|
+
(acc, key) => ({ ...acc, [`#${key}`]: key }),
|
|
97
|
+
{}
|
|
98
|
+
);
|
|
99
|
+
const expressionAttributeValues = Object.entries(toSet).reduce(
|
|
100
|
+
(acc, [key, value]) => ({ ...acc, [`:${key}`]: value }),
|
|
101
|
+
{}
|
|
102
|
+
);
|
|
103
|
+
await this.client.send(
|
|
104
|
+
new UpdateCommand({
|
|
105
|
+
TableName: this.tableName,
|
|
106
|
+
Key: { id },
|
|
107
|
+
UpdateExpression: updateExpression,
|
|
108
|
+
ExpressionAttributeNames: expressionAttributeNames,
|
|
109
|
+
...Object.keys(expressionAttributeValues).length > 0 && {
|
|
110
|
+
ExpressionAttributeValues: expressionAttributeValues
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
async deleteUser(id) {
|
|
116
|
+
await this.client.send(
|
|
117
|
+
new DeleteCommand({
|
|
118
|
+
TableName: this.tableName,
|
|
119
|
+
Key: { id }
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// src/storage/index.ts
|
|
126
|
+
function createStorageProvider(config) {
|
|
127
|
+
switch (config.type) {
|
|
128
|
+
case "dynamodb":
|
|
129
|
+
return new DynamoDBStorageProvider(config.options);
|
|
130
|
+
case "mongodb":
|
|
131
|
+
throw new Error("MongoDB storage provider not implemented yet");
|
|
132
|
+
case "postgres":
|
|
133
|
+
throw new Error("PostgreSQL storage provider not implemented yet");
|
|
134
|
+
default:
|
|
135
|
+
throw new Error(`Unsupported storage type: ${config.type}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/email/smtp.ts
|
|
140
|
+
import nodemailer from "nodemailer";
|
|
141
|
+
var SMTPEmailProvider = class {
|
|
142
|
+
constructor(from, config, templates) {
|
|
143
|
+
this.transporter = nodemailer.createTransport(config);
|
|
144
|
+
this.config = { from, templates };
|
|
145
|
+
}
|
|
146
|
+
async sendVerificationEmail(email, token) {
|
|
147
|
+
const { subject, html } = this.config.templates.verification;
|
|
148
|
+
await this.transporter.sendMail({
|
|
149
|
+
from: this.config.from,
|
|
150
|
+
to: email,
|
|
151
|
+
subject,
|
|
152
|
+
html: html(token)
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
async sendPasswordResetEmail(email, token) {
|
|
156
|
+
const { subject, html } = this.config.templates.resetPassword;
|
|
157
|
+
await this.transporter.sendMail({
|
|
158
|
+
from: this.config.from,
|
|
159
|
+
to: email,
|
|
160
|
+
subject,
|
|
161
|
+
html: html(token)
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
async verifyConnection() {
|
|
165
|
+
try {
|
|
166
|
+
await this.transporter.verify();
|
|
167
|
+
return true;
|
|
168
|
+
} catch (_error) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// src/email/ses.ts
|
|
175
|
+
import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2";
|
|
176
|
+
|
|
177
|
+
// src/email/defaultTemplates.ts
|
|
178
|
+
var defaultTemplates = {
|
|
179
|
+
verification: {
|
|
180
|
+
subject: "Verify your email address",
|
|
181
|
+
html: (token) => `
|
|
182
|
+
<!DOCTYPE html>
|
|
183
|
+
<html>
|
|
184
|
+
<head>
|
|
185
|
+
<meta charset="utf-8">
|
|
186
|
+
<title>Verify your email address</title>
|
|
187
|
+
<style>
|
|
188
|
+
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
189
|
+
.button { display: inline-block; padding: 12px 24px; background-color: #007bff; color: white; text-decoration: none; border-radius: 4px; margin: 20px 0; }
|
|
190
|
+
.footer { margin-top: 30px; font-size: 0.9em; color: #666; }
|
|
191
|
+
</style>
|
|
192
|
+
</head>
|
|
193
|
+
<body>
|
|
194
|
+
<h1>Verify your email address</h1>
|
|
195
|
+
<p>Thank you for signing up! Please click the button below to verify your email address:</p>
|
|
196
|
+
<a href="${token}" class="button">Verify Email Address</a>
|
|
197
|
+
<p>Or copy and paste this link in your browser:</p>
|
|
198
|
+
<p>${token}</p>
|
|
199
|
+
<div class="footer">
|
|
200
|
+
<p>If you didn't create an account, you can safely ignore this email.</p>
|
|
201
|
+
</div>
|
|
202
|
+
</body>
|
|
203
|
+
</html>`
|
|
204
|
+
},
|
|
205
|
+
resetPassword: {
|
|
206
|
+
subject: "Reset your password",
|
|
207
|
+
html: (token) => `
|
|
208
|
+
<!DOCTYPE html>
|
|
209
|
+
<html>
|
|
210
|
+
<head>
|
|
211
|
+
<meta charset="utf-8">
|
|
212
|
+
<title>Reset your password</title>
|
|
213
|
+
<style>
|
|
214
|
+
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
215
|
+
.button { display: inline-block; padding: 12px 24px; background-color: #007bff; color: white; text-decoration: none; border-radius: 4px; margin: 20px 0; }
|
|
216
|
+
.footer { margin-top: 30px; font-size: 0.9em; color: #666; }
|
|
217
|
+
</style>
|
|
218
|
+
</head>
|
|
219
|
+
<body>
|
|
220
|
+
<h1>Reset your password</h1>
|
|
221
|
+
<p>We received a request to reset your password. Click the button below to create a new password:</p>
|
|
222
|
+
<a href="${token}" class="button">Reset Password</a>
|
|
223
|
+
<p>Or copy and paste this link in your browser:</p>
|
|
224
|
+
<p>${token}</p>
|
|
225
|
+
<div class="footer">
|
|
226
|
+
<p>If you didn't request a password reset, you can safely ignore this email.</p>
|
|
227
|
+
<p>This link will expire in 24 hours.</p>
|
|
228
|
+
</div>
|
|
229
|
+
</body>
|
|
230
|
+
</html>`
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// src/email/ses.ts
|
|
235
|
+
var SESEmailProvider = class {
|
|
236
|
+
constructor(from, config, templates) {
|
|
237
|
+
this.client = new SESv2Client({
|
|
238
|
+
region: config.region,
|
|
239
|
+
credentials: config.credentials
|
|
240
|
+
});
|
|
241
|
+
this.config = {
|
|
242
|
+
from,
|
|
243
|
+
templates: templates ?? defaultTemplates,
|
|
244
|
+
sourceArn: config.sourceArn
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
async sendEmail(to, subject, html) {
|
|
248
|
+
const command = new SendEmailCommand({
|
|
249
|
+
FromEmailAddress: this.config.from,
|
|
250
|
+
FromEmailAddressIdentityArn: this.config.sourceArn,
|
|
251
|
+
Destination: {
|
|
252
|
+
ToAddresses: [to]
|
|
253
|
+
},
|
|
254
|
+
Content: {
|
|
255
|
+
Simple: {
|
|
256
|
+
Subject: {
|
|
257
|
+
Data: subject,
|
|
258
|
+
Charset: "UTF-8"
|
|
259
|
+
},
|
|
260
|
+
Body: {
|
|
261
|
+
Html: {
|
|
262
|
+
Data: html,
|
|
263
|
+
Charset: "UTF-8"
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
await this.client.send(command);
|
|
270
|
+
}
|
|
271
|
+
async sendVerificationEmail(email, token) {
|
|
272
|
+
const { subject, html } = this.config.templates.verification;
|
|
273
|
+
await this.sendEmail(email, subject, html(token));
|
|
274
|
+
}
|
|
275
|
+
async sendPasswordResetEmail(email, token) {
|
|
276
|
+
const { subject, html } = this.config.templates.resetPassword;
|
|
277
|
+
await this.sendEmail(email, subject, html(token));
|
|
278
|
+
}
|
|
279
|
+
async verifyConnection() {
|
|
280
|
+
try {
|
|
281
|
+
await this.client.send(
|
|
282
|
+
new SendEmailCommand({
|
|
283
|
+
FromEmailAddress: this.config.from,
|
|
284
|
+
FromEmailAddressIdentityArn: this.config.sourceArn,
|
|
285
|
+
Destination: {
|
|
286
|
+
ToAddresses: [this.config.from]
|
|
287
|
+
// Send to ourselves as a test
|
|
288
|
+
},
|
|
289
|
+
Content: {
|
|
290
|
+
Simple: {
|
|
291
|
+
Subject: { Data: "Test Connection", Charset: "UTF-8" },
|
|
292
|
+
Body: { Text: { Data: "Test", Charset: "UTF-8" } }
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
);
|
|
297
|
+
return true;
|
|
298
|
+
} catch (_error) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// src/email/index.ts
|
|
305
|
+
function createEmailProvider(config) {
|
|
306
|
+
switch (config.type) {
|
|
307
|
+
case "smtp":
|
|
308
|
+
return new SMTPEmailProvider(
|
|
309
|
+
config.from,
|
|
310
|
+
config.options,
|
|
311
|
+
config.templates
|
|
312
|
+
);
|
|
313
|
+
case "ses":
|
|
314
|
+
return new SESEmailProvider(config.from, config.options, config.templates);
|
|
315
|
+
default:
|
|
316
|
+
throw new Error(`Unsupported email provider type: ${config.type}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/services/auth.ts
|
|
321
|
+
var AuthService = class {
|
|
322
|
+
constructor(config) {
|
|
323
|
+
this.config = config;
|
|
324
|
+
this.storage = createStorageProvider(config.storage);
|
|
325
|
+
this.email = createEmailProvider(config.email);
|
|
326
|
+
}
|
|
327
|
+
generateToken(user) {
|
|
328
|
+
return jwt.sign(
|
|
329
|
+
{
|
|
330
|
+
id: user.id,
|
|
331
|
+
email: user.email,
|
|
332
|
+
roles: user.roles,
|
|
333
|
+
isEmailVerified: user.isEmailVerified
|
|
334
|
+
},
|
|
335
|
+
this.config.jwt.privateKey,
|
|
336
|
+
{
|
|
337
|
+
algorithm: "RS256",
|
|
338
|
+
expiresIn: this.config.jwt.expiresIn,
|
|
339
|
+
keyid: this.config.jwt.keyId
|
|
340
|
+
}
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
async hashPassword(password) {
|
|
344
|
+
return bcrypt.hash(password, 10);
|
|
345
|
+
}
|
|
346
|
+
async signup(email, password, roles = ["USER"]) {
|
|
347
|
+
const existingUser = await this.storage.getUserByEmail(email);
|
|
348
|
+
if (existingUser) {
|
|
349
|
+
throw new Error("User already exists");
|
|
350
|
+
}
|
|
351
|
+
const verificationToken = uuidv4();
|
|
352
|
+
const user = {
|
|
353
|
+
id: uuidv4(),
|
|
354
|
+
email,
|
|
355
|
+
passwordHash: await this.hashPassword(password),
|
|
356
|
+
roles,
|
|
357
|
+
isEmailVerified: false,
|
|
358
|
+
verificationToken,
|
|
359
|
+
createdAt: Date.now(),
|
|
360
|
+
updatedAt: Date.now()
|
|
361
|
+
};
|
|
362
|
+
await this.storage.createUser(user);
|
|
363
|
+
await this.email.sendVerificationEmail(email, verificationToken);
|
|
364
|
+
const token = this.generateToken(user);
|
|
365
|
+
return { user, token };
|
|
366
|
+
}
|
|
367
|
+
async signin(email, password) {
|
|
368
|
+
const user = await this.storage.getUserByEmail(email);
|
|
369
|
+
if (!user) {
|
|
370
|
+
throw new Error("Invalid credentials");
|
|
371
|
+
}
|
|
372
|
+
const isValidPassword = await bcrypt.compare(password, user.passwordHash);
|
|
373
|
+
if (!isValidPassword) {
|
|
374
|
+
throw new Error("Invalid credentials");
|
|
375
|
+
}
|
|
376
|
+
if (!user.isEmailVerified) {
|
|
377
|
+
throw new Error("Please verify your email before signing in");
|
|
378
|
+
}
|
|
379
|
+
const token = this.generateToken(user);
|
|
380
|
+
return { user, token };
|
|
381
|
+
}
|
|
382
|
+
async verifyEmail(token) {
|
|
383
|
+
const user = await this.storage.getUserByVerifyEmailToken(token);
|
|
384
|
+
if (!user || user.verificationToken !== token) {
|
|
385
|
+
throw new Error("Invalid verification token");
|
|
386
|
+
}
|
|
387
|
+
await this.storage.updateUser(user.id, {
|
|
388
|
+
isEmailVerified: true,
|
|
389
|
+
verificationToken: void 0,
|
|
390
|
+
updatedAt: Date.now()
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
async initiatePasswordReset(email) {
|
|
394
|
+
const user = await this.storage.getUserByEmail(email);
|
|
395
|
+
if (!user) {
|
|
396
|
+
throw new Error("User not found");
|
|
397
|
+
}
|
|
398
|
+
const resetToken = uuidv4();
|
|
399
|
+
const resetExpires = Date.now() + 36e5;
|
|
400
|
+
await this.storage.updateUser(user.id, {
|
|
401
|
+
resetPasswordToken: resetToken,
|
|
402
|
+
resetPasswordExpires: resetExpires,
|
|
403
|
+
updatedAt: Date.now()
|
|
404
|
+
});
|
|
405
|
+
await this.email.sendPasswordResetEmail(email, resetToken);
|
|
406
|
+
}
|
|
407
|
+
async resetPassword(token, newPassword) {
|
|
408
|
+
const user = await this.storage.getUserByResetPasswordToken(token);
|
|
409
|
+
if (!user || !user.resetPasswordToken || user.resetPasswordToken !== token || !user.resetPasswordExpires || user.resetPasswordExpires < Date.now()) {
|
|
410
|
+
throw new Error("Invalid or expired reset token");
|
|
411
|
+
}
|
|
412
|
+
await this.storage.updateUser(user.id, {
|
|
413
|
+
passwordHash: await this.hashPassword(newPassword),
|
|
414
|
+
resetPasswordToken: void 0,
|
|
415
|
+
resetPasswordExpires: void 0,
|
|
416
|
+
updatedAt: Date.now()
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
async verifyToken(token) {
|
|
420
|
+
try {
|
|
421
|
+
return jwt.verify(token, this.config.jwt.publicKey, { algorithms: ["RS256"] });
|
|
422
|
+
} catch (_error) {
|
|
423
|
+
throw new Error("Invalid token");
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
async hasRole(userId, requiredRoles) {
|
|
427
|
+
const user = await this.storage.getUserById(userId);
|
|
428
|
+
if (!user) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
return requiredRoles.some((role) => user.roles.includes(role));
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
// src/middleware/auth.ts
|
|
436
|
+
var AuthMiddleware = class {
|
|
437
|
+
constructor(authService) {
|
|
438
|
+
this.authService = authService;
|
|
439
|
+
}
|
|
440
|
+
verifyToken() {
|
|
441
|
+
return async (req, res, next) => {
|
|
442
|
+
try {
|
|
443
|
+
const token = req.headers.authorization?.split(" ")[1];
|
|
444
|
+
if (!token) {
|
|
445
|
+
throw new Error("No token provided");
|
|
446
|
+
}
|
|
447
|
+
const decoded = await this.authService.verifyToken(token);
|
|
448
|
+
req.user = decoded;
|
|
449
|
+
next();
|
|
450
|
+
} catch (_error) {
|
|
451
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
requireRoles(roles) {
|
|
456
|
+
return async (req, res, next) => {
|
|
457
|
+
try {
|
|
458
|
+
const hasRole = await this.authService.hasRole(req.user.id, roles);
|
|
459
|
+
if (!hasRole) {
|
|
460
|
+
throw new Error("Insufficient permissions");
|
|
461
|
+
}
|
|
462
|
+
next();
|
|
463
|
+
} catch (_error) {
|
|
464
|
+
res.status(403).json({ error: "Forbidden" });
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
requireEmailVerified() {
|
|
469
|
+
return (req, res, next) => {
|
|
470
|
+
if (!req.user.isEmailVerified) {
|
|
471
|
+
return res.status(403).json({ error: "Email not verified" });
|
|
472
|
+
}
|
|
473
|
+
next();
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// src/types/index.ts
|
|
479
|
+
import { z } from "zod";
|
|
480
|
+
var UserRoleSchema = z.enum(["ADMIN", "USER", "GUEST"]);
|
|
481
|
+
var UserSchema = z.object({
|
|
482
|
+
id: z.string(),
|
|
483
|
+
email: z.string().email(),
|
|
484
|
+
passwordHash: z.string(),
|
|
485
|
+
roles: z.array(UserRoleSchema),
|
|
486
|
+
isEmailVerified: z.boolean(),
|
|
487
|
+
verificationToken: z.string().optional(),
|
|
488
|
+
resetPasswordToken: z.string().optional(),
|
|
489
|
+
resetPasswordExpires: z.number().optional(),
|
|
490
|
+
createdAt: z.number(),
|
|
491
|
+
updatedAt: z.number()
|
|
492
|
+
});
|
|
493
|
+
export {
|
|
494
|
+
AuthMiddleware,
|
|
495
|
+
AuthService,
|
|
496
|
+
UserRoleSchema,
|
|
497
|
+
UserSchema
|
|
498
|
+
};
|
|
499
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/services/auth.ts","../src/storage/dynamodb.ts","../src/storage/index.ts","../src/email/smtp.ts","../src/email/ses.ts","../src/email/defaultTemplates.ts","../src/email/index.ts","../src/middleware/auth.ts","../src/types/index.ts"],"sourcesContent":["import bcrypt from 'bcryptjs';\nimport jwt from 'jsonwebtoken';\nimport { v4 as uuidv4 } from 'uuid';\nimport type { AuthConfig, User, UserRole } from '../types';\nimport type { IStorageProvider } from '../storage/types';\nimport type { IEmailProvider } from '../email/types';\nimport { createStorageProvider } from '../storage';\nimport { createEmailProvider } from '../email';\n\nexport class AuthService {\n private storage: IStorageProvider;\n private email: IEmailProvider;\n private config: AuthConfig;\n\n constructor(config: AuthConfig) {\n this.config = config;\n this.storage = createStorageProvider(config.storage);\n this.email = createEmailProvider(config.email);\n }\n\n private generateToken(user: User): string {\n // @ts-ignore\n return jwt.sign(\n {\n id: user.id,\n email: user.email,\n roles: user.roles,\n isEmailVerified: user.isEmailVerified,\n },\n this.config.jwt.privateKey,\n {\n algorithm: 'RS256',\n expiresIn: this.config.jwt.expiresIn,\n keyid: this.config.jwt.keyId,\n }\n );\n }\n\n private async hashPassword(password: string): Promise<string> {\n return bcrypt.hash(password, 10);\n }\n\n async signup(\n email: string,\n password: string,\n roles: UserRole[] = ['USER']\n ): Promise<{ user: User; token: string }> {\n const existingUser = await this.storage.getUserByEmail(email);\n if (existingUser) {\n throw new Error('User already exists');\n }\n\n const verificationToken = uuidv4();\n const user: User = {\n id: uuidv4(),\n email,\n passwordHash: await this.hashPassword(password),\n roles,\n isEmailVerified: false,\n verificationToken,\n createdAt: Date.now(),\n updatedAt: Date.now(),\n };\n\n await this.storage.createUser(user);\n await this.email.sendVerificationEmail(email, verificationToken);\n\n const token = this.generateToken(user);\n return { user, token };\n }\n\n async signin(email: string, password: string): Promise<{ user: User; token: string }> {\n const user = await this.storage.getUserByEmail(email);\n if (!user) {\n throw new Error('Invalid credentials');\n }\n\n const isValidPassword = await bcrypt.compare(password, user.passwordHash);\n if (!isValidPassword) {\n throw new Error('Invalid credentials');\n }\n\n if (!user.isEmailVerified) {\n throw new Error('Please verify your email before signing in');\n }\n\n const token = this.generateToken(user);\n return { user, token };\n }\n\n async verifyEmail(token: string): Promise<void> {\n const user = await this.storage.getUserByVerifyEmailToken(token);\n if (!user || user.verificationToken !== token) {\n throw new Error('Invalid verification token');\n }\n\n await this.storage.updateUser(user.id, {\n isEmailVerified: true,\n verificationToken: undefined,\n updatedAt: Date.now(),\n });\n }\n\n async initiatePasswordReset(email: string): Promise<void> {\n const user = await this.storage.getUserByEmail(email);\n if (!user) {\n throw new Error('User not found');\n }\n\n const resetToken = uuidv4();\n const resetExpires = Date.now() + 3600000; // 1 hour\n\n await this.storage.updateUser(user.id, {\n resetPasswordToken: resetToken,\n resetPasswordExpires: resetExpires,\n updatedAt: Date.now(),\n });\n\n await this.email.sendPasswordResetEmail(email, resetToken);\n }\n\n async resetPassword(token: string, newPassword: string): Promise<void> {\n const user = await this.storage.getUserByResetPasswordToken(token);\n if (\n !user ||\n !user.resetPasswordToken ||\n user.resetPasswordToken !== token ||\n !user.resetPasswordExpires ||\n user.resetPasswordExpires < Date.now()\n ) {\n throw new Error('Invalid or expired reset token');\n }\n\n await this.storage.updateUser(user.id, {\n passwordHash: await this.hashPassword(newPassword),\n resetPasswordToken: undefined,\n resetPasswordExpires: undefined,\n updatedAt: Date.now(),\n });\n }\n\n async verifyToken(\n token: string\n ): Promise<{ id: string; email: string; roles: UserRole[]; isEmailVerified: boolean }> {\n try {\n return jwt.verify(token, this.config.jwt.publicKey, { algorithms: ['RS256'] }) as {\n id: string;\n email: string;\n roles: UserRole[];\n isEmailVerified: boolean;\n };\n } catch (_error) {\n throw new Error('Invalid token');\n }\n }\n\n async hasRole(userId: string, requiredRoles: UserRole[]): Promise<boolean> {\n const user = await this.storage.getUserById(userId);\n if (!user) {\n return false;\n }\n\n return requiredRoles.some((role) => user.roles.includes(role));\n }\n}\n","import { DynamoDBClient } from '@aws-sdk/client-dynamodb';\nimport {\n DynamoDBDocumentClient,\n PutCommand,\n GetCommand,\n UpdateCommand,\n DeleteCommand,\n QueryCommand,\n} from '@aws-sdk/lib-dynamodb';\nimport type { User } from '../types';\nimport type { IStorageProvider, DynamoDBConfig } from './types';\n\nexport class DynamoDBStorageProvider implements IStorageProvider {\n private client: DynamoDBDocumentClient;\n private tableName: string;\n\n constructor(config: DynamoDBConfig) {\n const dbClient = new DynamoDBClient({ region: config.region });\n this.client = DynamoDBDocumentClient.from(dbClient, {});\n this.tableName = config.tableName;\n }\n\n async createUser(user: User): Promise<void> {\n await this.client.send(\n new PutCommand({\n TableName: this.tableName,\n Item: user,\n ConditionExpression: 'attribute_not_exists(email)',\n })\n );\n }\n\n async getUserByResetPasswordToken(resetPasswordToken: string): Promise<User | null> {\n const response = await this.client.send(\n new QueryCommand({\n TableName: this.tableName,\n IndexName: 'ResetPasswordTokenIndex',\n KeyConditionExpression: 'resetPasswordToken = :token',\n ExpressionAttributeValues: {\n ':token': resetPasswordToken,\n },\n })\n );\n\n return response.Items?.[0] as User | null;\n }\n\n async getUserByVerifyEmailToken(verifyEmailToken: string): Promise<User | null> {\n const response = await this.client.send(\n new QueryCommand({\n TableName: this.tableName,\n IndexName: 'VerifyEmailTokenIndex',\n KeyConditionExpression: 'verificationToken = :token',\n ExpressionAttributeValues: {\n ':token': verifyEmailToken,\n },\n })\n );\n\n return response.Items?.[0] as User | null;\n }\n\n async getUserById(id: string): Promise<User | null> {\n const response = await this.client.send(\n new GetCommand({\n TableName: this.tableName,\n Key: { id },\n })\n );\n\n return response.Item as User | null;\n }\n\n async getUserByEmail(email: string): Promise<User | null> {\n const response = await this.client.send(\n new QueryCommand({\n TableName: this.tableName,\n IndexName: 'EmailIndex',\n KeyConditionExpression: 'email = :email',\n ExpressionAttributeValues: {\n ':email': email,\n },\n })\n );\n\n return response.Items?.[0] as User | null;\n }\n\n async updateUser(id: string, updates: Partial<User>): Promise<void> {\n const toSet: Record<string, any> = {};\n const toRemove: string[] = [];\n\n // Separate attributes to set and remove\n Object.entries(updates).forEach(([key, value]) => {\n if (value === undefined) {\n toRemove.push(key);\n } else {\n toSet[key] = value;\n }\n });\n\n // If no updates at all, return early\n if (Object.keys(toSet).length === 0 && toRemove.length === 0) {\n return;\n }\n\n // Build the update expression\n const setPart =\n Object.keys(toSet).length > 0\n ? `SET ${Object.keys(toSet)\n .map((key) => `#${key} = :${key}`)\n .join(', ')}`\n : '';\n\n const removePart =\n toRemove.length > 0 ? `REMOVE ${toRemove.map((key) => `#${key}`).join(', ')}` : '';\n\n const updateExpression = [setPart, removePart].filter(Boolean).join(' ');\n\n // Build expression attribute names (needed for both SET and REMOVE)\n const expressionAttributeNames = [...Object.keys(toSet), ...toRemove].reduce(\n (acc, key) => ({ ...acc, [`#${key}`]: key }),\n {}\n );\n\n // Build expression attribute values (only needed for SET)\n const expressionAttributeValues = Object.entries(toSet).reduce(\n (acc, [key, value]) => ({ ...acc, [`:${key}`]: value }),\n {}\n );\n\n await this.client.send(\n new UpdateCommand({\n TableName: this.tableName,\n Key: { id },\n UpdateExpression: updateExpression,\n ExpressionAttributeNames: expressionAttributeNames,\n ...(Object.keys(expressionAttributeValues).length > 0 && {\n ExpressionAttributeValues: expressionAttributeValues,\n }),\n })\n );\n }\n\n async deleteUser(id: string): Promise<void> {\n await this.client.send(\n new DeleteCommand({\n TableName: this.tableName,\n Key: { id },\n })\n );\n }\n}\n","import type { DynamoDBConfig, IStorageProvider, StorageConfig } from './types';\nimport { DynamoDBStorageProvider } from './dynamodb';\n\nexport function createStorageProvider(config: StorageConfig): IStorageProvider {\n switch (config.type) {\n case 'dynamodb':\n return new DynamoDBStorageProvider(config.options as DynamoDBConfig);\n case 'mongodb':\n throw new Error('MongoDB storage provider not implemented yet');\n case 'postgres':\n throw new Error('PostgreSQL storage provider not implemented yet');\n default:\n throw new Error(`Unsupported storage type: ${config.type}`);\n }\n}\n\nexport * from './types';\nexport * from './dynamodb';\n","import nodemailer from 'nodemailer';\nimport type { IEmailProvider, SMTPConfig, EmailTemplates } from './types';\n\nexport class SMTPEmailProvider implements IEmailProvider {\n private transporter: nodemailer.Transporter;\n private config: { from: string; templates: EmailTemplates };\n\n constructor(from: string, config: SMTPConfig, templates: EmailTemplates) {\n this.transporter = nodemailer.createTransport(config);\n this.config = { from, templates };\n }\n\n async sendVerificationEmail(email: string, token: string): Promise<void> {\n const { subject, html } = this.config.templates.verification;\n\n await this.transporter.sendMail({\n from: this.config.from,\n to: email,\n subject,\n html: html(token),\n });\n }\n\n async sendPasswordResetEmail(email: string, token: string): Promise<void> {\n const { subject, html } = this.config.templates.resetPassword;\n\n await this.transporter.sendMail({\n from: this.config.from,\n to: email,\n subject,\n html: html(token),\n });\n }\n\n async verifyConnection(): Promise<boolean> {\n try {\n await this.transporter.verify();\n return true;\n } catch (_error) {\n return false;\n }\n }\n}\n","import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2';\nimport type { IEmailProvider, SESConfig, EmailTemplates } from './types';\nimport { defaultTemplates } from './defaultTemplates';\n\nexport class SESEmailProvider implements IEmailProvider {\n private client: SESv2Client;\n private config: { from: string; templates: EmailTemplates; sourceArn?: string };\n\n constructor(from: string, config: SESConfig, templates?: EmailTemplates) {\n this.client = new SESv2Client({\n region: config.region,\n credentials: config.credentials,\n });\n this.config = {\n from,\n templates: templates ?? defaultTemplates,\n sourceArn: config.sourceArn,\n };\n }\n\n private async sendEmail(to: string, subject: string, html: string): Promise<void> {\n const command = new SendEmailCommand({\n FromEmailAddress: this.config.from,\n FromEmailAddressIdentityArn: this.config.sourceArn,\n Destination: {\n ToAddresses: [to],\n },\n Content: {\n Simple: {\n Subject: {\n Data: subject,\n Charset: 'UTF-8',\n },\n Body: {\n Html: {\n Data: html,\n Charset: 'UTF-8',\n },\n },\n },\n },\n });\n\n await this.client.send(command);\n }\n\n async sendVerificationEmail(email: string, token: string): Promise<void> {\n const { subject, html } = this.config.templates.verification;\n await this.sendEmail(email, subject, html(token));\n }\n\n async sendPasswordResetEmail(email: string, token: string): Promise<void> {\n const { subject, html } = this.config.templates.resetPassword;\n await this.sendEmail(email, subject, html(token));\n }\n\n async verifyConnection(): Promise<boolean> {\n try {\n // SES doesn't have a direct verify method, so we'll check if we can describe our sending status\n await this.client.send(\n new SendEmailCommand({\n FromEmailAddress: this.config.from,\n FromEmailAddressIdentityArn: this.config.sourceArn,\n Destination: {\n ToAddresses: [this.config.from], // Send to ourselves as a test\n },\n Content: {\n Simple: {\n Subject: { Data: 'Test Connection', Charset: 'UTF-8' },\n Body: { Text: { Data: 'Test', Charset: 'UTF-8' } },\n },\n },\n })\n );\n return true;\n } catch (_error) {\n return false;\n }\n }\n}\n","import type { EmailTemplates } from './types';\n\nexport const defaultTemplates: EmailTemplates = {\n verification: {\n subject: 'Verify your email address',\n html: (token: string) => `\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>Verify your email address</title>\n <style>\n body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }\n .button { display: inline-block; padding: 12px 24px; background-color: #007bff; color: white; text-decoration: none; border-radius: 4px; margin: 20px 0; }\n .footer { margin-top: 30px; font-size: 0.9em; color: #666; }\n </style>\n</head>\n<body>\n <h1>Verify your email address</h1>\n <p>Thank you for signing up! Please click the button below to verify your email address:</p>\n <a href=\"${token}\" class=\"button\">Verify Email Address</a>\n <p>Or copy and paste this link in your browser:</p>\n <p>${token}</p>\n <div class=\"footer\">\n <p>If you didn't create an account, you can safely ignore this email.</p>\n </div>\n</body>\n</html>`,\n },\n resetPassword: {\n subject: 'Reset your password',\n html: (token: string) => `\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>Reset your password</title>\n <style>\n body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }\n .button { display: inline-block; padding: 12px 24px; background-color: #007bff; color: white; text-decoration: none; border-radius: 4px; margin: 20px 0; }\n .footer { margin-top: 30px; font-size: 0.9em; color: #666; }\n </style>\n</head>\n<body>\n <h1>Reset your password</h1>\n <p>We received a request to reset your password. Click the button below to create a new password:</p>\n <a href=\"${token}\" class=\"button\">Reset Password</a>\n <p>Or copy and paste this link in your browser:</p>\n <p>${token}</p>\n <div class=\"footer\">\n <p>If you didn't request a password reset, you can safely ignore this email.</p>\n <p>This link will expire in 24 hours.</p>\n </div>\n</body>\n</html>`,\n },\n};\n","import type { IEmailProvider, EmailConfig, SMTPConfig, SESConfig } from './types';\nimport { SMTPEmailProvider } from './smtp';\nimport { SESEmailProvider } from './ses';\n\nexport function createEmailProvider(config: EmailConfig): IEmailProvider {\n switch (config.type) {\n case 'smtp':\n return new SMTPEmailProvider(\n config.from,\n config.options as SMTPConfig,\n config.templates\n );\n case 'ses':\n return new SESEmailProvider(config.from, config.options as SESConfig, config.templates);\n default:\n throw new Error(`Unsupported email provider type: ${config.type}`);\n }\n}\n\nexport * from './types';\nexport * from './smtp';\nexport * from './ses';\n","import type { AuthService } from '../services/auth';\nimport type { UserRole } from '../types';\n\nexport class AuthMiddleware {\n private authService: AuthService;\n\n constructor(authService: AuthService) {\n this.authService = authService;\n }\n\n verifyToken() {\n return async (req: any, res: any, next: any) => {\n try {\n const token = req.headers.authorization?.split(' ')[1];\n if (!token) {\n throw new Error('No token provided');\n }\n\n const decoded = await this.authService.verifyToken(token);\n req.user = decoded;\n next();\n } catch (_error) {\n res.status(401).json({ error: 'Unauthorized' });\n }\n };\n }\n\n requireRoles(roles: UserRole[]) {\n return async (req: any, res: any, next: any) => {\n try {\n const hasRole = await this.authService.hasRole(req.user.id, roles);\n if (!hasRole) {\n throw new Error('Insufficient permissions');\n }\n next();\n } catch (_error) {\n res.status(403).json({ error: 'Forbidden' });\n }\n };\n }\n\n requireEmailVerified() {\n return (req: any, res: any, next: any) => {\n if (!req.user.isEmailVerified) {\n return res.status(403).json({ error: 'Email not verified' });\n }\n next();\n };\n }\n}\n","import { z } from 'zod';\nimport type { StorageConfig } from '../storage/types';\nimport type { EmailConfig } from '../email/types';\n\nexport const UserRoleSchema = z.enum(['ADMIN', 'USER', 'GUEST']);\nexport type UserRole = z.infer<typeof UserRoleSchema>;\n\nexport const UserSchema = z.object({\n id: z.string(),\n email: z.string().email(),\n passwordHash: z.string(),\n roles: z.array(UserRoleSchema),\n isEmailVerified: z.boolean(),\n verificationToken: z.string().optional(),\n resetPasswordToken: z.string().optional(),\n resetPasswordExpires: z.number().optional(),\n createdAt: z.number(),\n updatedAt: z.number(),\n});\n\nexport type User = z.infer<typeof UserSchema>;\n\nexport interface AuthConfig {\n jwt: {\n privateKey: string;\n publicKey: string;\n keyId: string;\n expiresIn: string;\n };\n storage: StorageConfig;\n email: EmailConfig;\n}\n"],"mappings":"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","names":[]}
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xcelsior/auth",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Reusable serverless authentication system with RBAC and configurable email notifications",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"import": "./src/index.ts",
|
|
8
|
+
"require": "./dist/index.js"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"main": "src/index.ts",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@aws-sdk/client-dynamodb": "^3.0.0",
|
|
14
|
+
"@aws-sdk/client-sesv2": "^3.0.0",
|
|
15
|
+
"@aws-sdk/lib-dynamodb": "^3.0.0",
|
|
16
|
+
"@aws-sdk/util-dynamodb": "^3.0.0",
|
|
17
|
+
"bcryptjs": "^2.4.3",
|
|
18
|
+
"jsonwebtoken": "^9.0.0",
|
|
19
|
+
"nodemailer": "^6.9.0",
|
|
20
|
+
"uuid": "^9.0.0",
|
|
21
|
+
"zod": "^3.22.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/bcryptjs": "^2.4.0",
|
|
25
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
26
|
+
"@types/node": "^20.0.0",
|
|
27
|
+
"@types/nodemailer": "^6.4.17",
|
|
28
|
+
"@types/uuid": "^9.0.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@aws-sdk/client-dynamodb": "^3.0.0",
|
|
32
|
+
"@aws-sdk/lib-dynamodb": "^3.0.0"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup",
|
|
36
|
+
"dev": "tsup --watch",
|
|
37
|
+
"test": "jest --passWithNoTests",
|
|
38
|
+
"lint": "biome check ."
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { EmailTemplates } from './types';
|
|
2
|
+
|
|
3
|
+
export const defaultTemplates: EmailTemplates = {
|
|
4
|
+
verification: {
|
|
5
|
+
subject: 'Verify your email address',
|
|
6
|
+
html: (token: string) => `
|
|
7
|
+
<!DOCTYPE html>
|
|
8
|
+
<html>
|
|
9
|
+
<head>
|
|
10
|
+
<meta charset="utf-8">
|
|
11
|
+
<title>Verify your email address</title>
|
|
12
|
+
<style>
|
|
13
|
+
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
14
|
+
.button { display: inline-block; padding: 12px 24px; background-color: #007bff; color: white; text-decoration: none; border-radius: 4px; margin: 20px 0; }
|
|
15
|
+
.footer { margin-top: 30px; font-size: 0.9em; color: #666; }
|
|
16
|
+
</style>
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
<h1>Verify your email address</h1>
|
|
20
|
+
<p>Thank you for signing up! Please click the button below to verify your email address:</p>
|
|
21
|
+
<a href="${token}" class="button">Verify Email Address</a>
|
|
22
|
+
<p>Or copy and paste this link in your browser:</p>
|
|
23
|
+
<p>${token}</p>
|
|
24
|
+
<div class="footer">
|
|
25
|
+
<p>If you didn't create an account, you can safely ignore this email.</p>
|
|
26
|
+
</div>
|
|
27
|
+
</body>
|
|
28
|
+
</html>`,
|
|
29
|
+
},
|
|
30
|
+
resetPassword: {
|
|
31
|
+
subject: 'Reset your password',
|
|
32
|
+
html: (token: string) => `
|
|
33
|
+
<!DOCTYPE html>
|
|
34
|
+
<html>
|
|
35
|
+
<head>
|
|
36
|
+
<meta charset="utf-8">
|
|
37
|
+
<title>Reset your password</title>
|
|
38
|
+
<style>
|
|
39
|
+
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
40
|
+
.button { display: inline-block; padding: 12px 24px; background-color: #007bff; color: white; text-decoration: none; border-radius: 4px; margin: 20px 0; }
|
|
41
|
+
.footer { margin-top: 30px; font-size: 0.9em; color: #666; }
|
|
42
|
+
</style>
|
|
43
|
+
</head>
|
|
44
|
+
<body>
|
|
45
|
+
<h1>Reset your password</h1>
|
|
46
|
+
<p>We received a request to reset your password. Click the button below to create a new password:</p>
|
|
47
|
+
<a href="${token}" class="button">Reset Password</a>
|
|
48
|
+
<p>Or copy and paste this link in your browser:</p>
|
|
49
|
+
<p>${token}</p>
|
|
50
|
+
<div class="footer">
|
|
51
|
+
<p>If you didn't request a password reset, you can safely ignore this email.</p>
|
|
52
|
+
<p>This link will expire in 24 hours.</p>
|
|
53
|
+
</div>
|
|
54
|
+
</body>
|
|
55
|
+
</html>`,
|
|
56
|
+
},
|
|
57
|
+
};
|