feat(auth): signupm, forgot passowrd, reset password all added
This commit is contained in:
38
app/src/internal/auth/routes/resetPassword.ts
Normal file
38
app/src/internal/auth/routes/resetPassword.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Router, type Request, type Response } from "express";
|
||||
|
||||
import { authClient } from "../../../pkg/auth/auth-client.js";
|
||||
import z from "zod";
|
||||
import { auth } from "../../../pkg/auth/auth.js";
|
||||
|
||||
const resetSchema = z.object({
|
||||
email: z.string(),
|
||||
});
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const validated = resetSchema.parse(req.body);
|
||||
|
||||
const data = await auth.api.requestPasswordReset({
|
||||
body: {
|
||||
email: validated.email,
|
||||
redirectTo: `${process.env.BETTER_AUTH_URL}/user/resetpassword`,
|
||||
},
|
||||
});
|
||||
return res.json(data);
|
||||
} catch (err) {
|
||||
if (err instanceof z.ZodError) {
|
||||
const flattened = z.flattenError(err);
|
||||
return res.status(400).json({
|
||||
error: "Validation failed",
|
||||
details: flattened,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(err);
|
||||
return res.status(400).json({ error: err });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -2,10 +2,12 @@ import type { Express, Request, Response } from "express";
|
||||
import me from "./me.js";
|
||||
import register from "./register.js";
|
||||
import userRoles from "./userroles.js";
|
||||
import resetPassword from "./resetPassword.js";
|
||||
import { requireAuth } from "../../../pkg/middleware/authMiddleware.js";
|
||||
|
||||
export const setupAuthRoutes = (app: Express, basePath: string) => {
|
||||
app.use(basePath + "/api/user/me", requireAuth(), me);
|
||||
app.use(basePath + "/api/user/resetpassword", resetPassword);
|
||||
app.use(basePath + "/api/user/register", register);
|
||||
app.use(basePath + "/api/user/roles", requireAuth(), userRoles);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import type { auth } from "./auth.js";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: "http://localhost:3000",
|
||||
baseURL: process.env.BETTER_AUTH_URL,
|
||||
plugins: [
|
||||
inferAdditionalFields<typeof auth>(),
|
||||
usernameClient(),
|
||||
|
||||
@@ -5,6 +5,7 @@ import { betterAuth } from "better-auth";
|
||||
import * as rawSchema from "../db/schema/auth-schema.js";
|
||||
import type { User } from "better-auth/types";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { sendEmail } from "../utils/mail/sendMail.js";
|
||||
|
||||
export const schema = {
|
||||
user: rawSchema.user,
|
||||
@@ -14,6 +15,7 @@ export const schema = {
|
||||
jwks: rawSchema.jwks,
|
||||
apiKey: rawSchema.apikey, // 🔑 rename to apiKey
|
||||
};
|
||||
const RESET_EXPIRY_SECONDS = 3600; // 1 hour
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
@@ -24,11 +26,40 @@ export const auth = betterAuth({
|
||||
"*.alpla.net",
|
||||
"http://localhost:5173",
|
||||
"http://localhost:5500",
|
||||
"http://localhost:4200",
|
||||
"http://localhost:4000",
|
||||
],
|
||||
appName: "lst",
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
minPasswordLength: 8, // optional config
|
||||
resetPasswordTokenExpirySeconds: RESET_EXPIRY_SECONDS, // time in seconds
|
||||
sendResetPassword: async ({ user, token }) => {
|
||||
const frontendUrl = `${process.env.BETTER_AUTH_URL}/lst/app/user/resetpassword?token=${token}`;
|
||||
const expiryMinutes = Math.floor(RESET_EXPIRY_SECONDS / 60);
|
||||
const expiryText =
|
||||
expiryMinutes >= 60
|
||||
? `${expiryMinutes / 60} hour${
|
||||
expiryMinutes === 60 ? "" : "s"
|
||||
}`
|
||||
: `${expiryMinutes} minutes`;
|
||||
const emailData = {
|
||||
email: user.email,
|
||||
subject: "LST- Forgot password request",
|
||||
template: "forgotPassword",
|
||||
context: {
|
||||
username: user.name,
|
||||
email: user.email,
|
||||
url: frontendUrl,
|
||||
expiry: expiryText,
|
||||
},
|
||||
};
|
||||
await sendEmail(emailData);
|
||||
},
|
||||
// onPasswordReset: async ({ user }, request) => {
|
||||
// // your logic here
|
||||
// console.log(`Password for user ${user.email} has been reset.`);
|
||||
// },
|
||||
},
|
||||
plugins: [
|
||||
//jwt({ jwt: { expirationTime: "1h" } }),
|
||||
|
||||
@@ -2,6 +2,24 @@ import type { Request, Response, NextFunction } from "express";
|
||||
import { db } from "../db/db.js";
|
||||
import { apiHits } from "../db/schema/apiHits.js";
|
||||
|
||||
type StripPasswords<T> = T extends Array<infer U>
|
||||
? StripPasswords<U>[]
|
||||
: T extends object
|
||||
? { [K in keyof T as Exclude<K, "password">]: StripPasswords<T[K]> }
|
||||
: T;
|
||||
|
||||
function stripPasswords<T>(input: T): StripPasswords<T> {
|
||||
if (Array.isArray(input)) {
|
||||
return input.map(stripPasswords) as StripPasswords<T>;
|
||||
} else if (input && typeof input === "object") {
|
||||
const entries = Object.entries(input as object)
|
||||
.filter(([key]) => key !== "password")
|
||||
.map(([key, value]) => [key, stripPasswords(value)]);
|
||||
return Object.fromEntries(entries) as StripPasswords<T>;
|
||||
}
|
||||
return input as StripPasswords<T>;
|
||||
}
|
||||
|
||||
export function apiHitMiddleware(
|
||||
req: Request,
|
||||
res: Response,
|
||||
@@ -15,7 +33,7 @@ export function apiHitMiddleware(
|
||||
await db.insert(apiHits).values({
|
||||
method: req.method,
|
||||
path: req.originalUrl,
|
||||
body: JSON.stringify(req.body ?? {}),
|
||||
body: JSON.stringify(req.body ? stripPasswords(req.body) : {}),
|
||||
status: res.statusCode,
|
||||
ip: req.ip,
|
||||
duration: Date.now() - start,
|
||||
|
||||
123
app/src/pkg/utils/mail/sendMail.ts
Normal file
123
app/src/pkg/utils/mail/sendMail.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { Address } from "nodemailer/lib/mailer/index.js";
|
||||
import type { Transporter } from "nodemailer";
|
||||
import type SMTPTransport from "nodemailer/lib/smtp-transport/index.js";
|
||||
import type Mail from "nodemailer/lib/mailer/index.js";
|
||||
import os from "os";
|
||||
import nodemailer from "nodemailer";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { promisify } from "util";
|
||||
import hbs from "nodemailer-express-handlebars";
|
||||
import { createLogger } from "../../logger/logger.js";
|
||||
|
||||
interface HandlebarsMailOptions extends Mail.Options {
|
||||
template: string;
|
||||
context: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface EmailData {
|
||||
email: string;
|
||||
subject: string;
|
||||
template: string;
|
||||
context: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export const sendEmail = async (data: EmailData): Promise<any> => {
|
||||
const log = createLogger({ module: "pkg", subModule: "sendMail" });
|
||||
let transporter: Transporter;
|
||||
let fromEmail: string | Address;
|
||||
|
||||
if (
|
||||
os.hostname().includes("OLP") &&
|
||||
process.env.EMAIL_USER &&
|
||||
process.env.EMAIL_PASSWORD
|
||||
) {
|
||||
transporter = nodemailer.createTransport({
|
||||
service: "gmail",
|
||||
auth: {
|
||||
user: process.env.EMAIL_USER,
|
||||
pass: process.env.EMAIL_PASSWORD,
|
||||
},
|
||||
//debug: true,
|
||||
});
|
||||
|
||||
// update the from email
|
||||
fromEmail = process.env.EMAIL_USER;
|
||||
} else {
|
||||
// convert to the correct plant token.
|
||||
|
||||
let host = `${os.hostname().replace("VMS006", "")}-smtp.alpla.net`;
|
||||
|
||||
//const testServers = ["vms036", "VMS036"];
|
||||
|
||||
if (os.hostname().includes("VMS036")) {
|
||||
host = "USMCD1-smtp.alpla.net";
|
||||
}
|
||||
|
||||
// if (plantToken[0].value === "usiow2") {
|
||||
// host = "USIOW1-smtp.alpla.net";
|
||||
// }
|
||||
|
||||
transporter = nodemailer.createTransport({
|
||||
host: host,
|
||||
port: 25,
|
||||
rejectUnauthorized: false,
|
||||
//secure: false,
|
||||
// auth: {
|
||||
// user: "alplaprod",
|
||||
// pass: "obelix",
|
||||
// },
|
||||
debug: true,
|
||||
} as SMTPTransport.Options);
|
||||
|
||||
// update the from email
|
||||
fromEmail = `noreply@alpla.com`;
|
||||
}
|
||||
|
||||
// creating the handlbar options
|
||||
const viewPath = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"./views/"
|
||||
);
|
||||
|
||||
const handlebarOptions = {
|
||||
viewEngine: {
|
||||
extname: ".hbs",
|
||||
//layoutsDir: path.resolve(viewPath, "layouts"), // Path to layouts directory
|
||||
defaultLayout: "", // Specify the default layout
|
||||
partialsDir: viewPath,
|
||||
},
|
||||
viewPath: viewPath,
|
||||
extName: ".hbs", // File extension for Handlebars templates
|
||||
};
|
||||
|
||||
transporter.use("compile", hbs(handlebarOptions));
|
||||
|
||||
const mailOptions: HandlebarsMailOptions = {
|
||||
from: fromEmail,
|
||||
to: data.email,
|
||||
subject: data.subject,
|
||||
//text: "You will have a reset token here and only have 30min to click the link before it expires.",
|
||||
//html: emailTemplate("BlakesTest", "This is an example with css"),
|
||||
template: data.template, // Name of the Handlebars template (e.g., 'welcome.hbs')
|
||||
context: data.context,
|
||||
};
|
||||
|
||||
// now verify and send the email
|
||||
const sendMailPromise = promisify(transporter.sendMail).bind(transporter);
|
||||
|
||||
try {
|
||||
// Send email and await the result
|
||||
const info = await sendMailPromise(mailOptions);
|
||||
log.info(null, `Email was sent to: ${data.email}`);
|
||||
return { success: true, message: "Email sent.", data: info };
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
log.error(
|
||||
{ error: err },
|
||||
|
||||
`Error sending Email to : ${data.email}`
|
||||
);
|
||||
return { success: false, message: "Error sending email.", error: err };
|
||||
}
|
||||
};
|
||||
69
app/src/pkg/utils/mail/views/forgotPassword.hbs
Normal file
69
app/src/pkg/utils/mail/views/forgotPassword.hbs
Normal file
@@ -0,0 +1,69 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Password Reset</title>
|
||||
{{> styles}}
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
color: #333333;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
.email-container {
|
||||
max-width: 600px;
|
||||
margin: 40px auto;
|
||||
padding: 20px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
||||
}
|
||||
h2 {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
margin: 20px 0;
|
||||
font-size: 16px;
|
||||
color: #ffffff;
|
||||
background-color: #4f46e5;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.footer {
|
||||
font-size: 13px;
|
||||
color: #666666;
|
||||
margin-top: 30px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<h2>Password Reset Request</h2>
|
||||
<p>Hi {{username}},</p>
|
||||
|
||||
<p>We received a request to reset the password for your account ({{email}}). If this was you, you can create a new password by clicking the button below:</p>
|
||||
|
||||
<p style="text-align:center;">
|
||||
<a href="{{url}}" class="btn">Reset Your Password</a>
|
||||
</p>
|
||||
|
||||
<p>If the button above doesn’t work, copy and paste the following link into your web browser:</p>
|
||||
<p style="word-break: break-all; color:#4f46e5;">{{url}}</p>
|
||||
|
||||
<p><strong>Note:</strong> This link will expire in <b>{{expiry}}</b> for your security.</p>
|
||||
|
||||
<p>If you did not request a password reset, you can safely ignore this email — your account will remain secure.</p>
|
||||
|
||||
<p>Thanks,<br/>The LST Team</p>
|
||||
|
||||
<div class="footer">
|
||||
<p>You are receiving this email because a password reset was requested for your account.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
6
app/src/pkg/utils/mail/views/styles.hbs
Normal file
6
app/src/pkg/utils/mail/views/styles.hbs
Normal file
@@ -0,0 +1,6 @@
|
||||
<style>
|
||||
table { width: 100%; background-color: #ffffff; border-collapse: collapse;
|
||||
border-width: 2px; border-color: #14BDEA; border-style: solid; color:
|
||||
#000000; } th, td { border: 1px solid #ddd; padding: 8px; } th {
|
||||
background-color: #f4f4f4; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user