diff --git a/app-next/src/lib/email-templates/admin-upload.ts b/app-next/src/lib/email-templates/admin-upload.ts new file mode 100644 index 00000000..e07e55e3 --- /dev/null +++ b/app-next/src/lib/email-templates/admin-upload.ts @@ -0,0 +1,46 @@ +// Email specific components +import { baseLayout } from "./layout"; + +export const generateAdminReviewEmail = ( + userName: string, + datasetId: string, + datasetName: string, + logoUrl: string, + datasetUrl?: string, +) => { + const datasetLink = datasetUrl ?? `https://www.openml.org/datasets/${datasetId}`; + + const content = ` + +

New Dataset Uploaded

+ +
+

Upload Details

+ + + + + + + + + + + + + +
User:${userName}
Dataset:${datasetName} (ID: ${datasetId})
Time:${new Date().toLocaleString()}
+ + +
+ Review Dataset +
+
+ +

+ This is an automated notification for OpenML Admins. +

+ `; + + return baseLayout(content, logoUrl); +}; diff --git a/app-next/src/lib/email-templates/confirm-email.ts b/app-next/src/lib/email-templates/confirm-email.ts new file mode 100644 index 00000000..9e43235e --- /dev/null +++ b/app-next/src/lib/email-templates/confirm-email.ts @@ -0,0 +1,33 @@ +// Email specific components +import { baseLayout } from "./layout"; + +export const generateConfirmEmail = (link: string, logoUrl: string) => { + const content = ` + +

Welcome to OpenML!

+ +

Hi there,

+

Thank you for signing up to OpenML, the collaborative machine learning platform.

+

To verify your email address and activate your account, please click the button below:

+ + +
+ Confirm Email Address +
+ + +
+

Or copy and paste this link into your browser:

+

${link}

+
+ +

+ ⏱️ This confirmation link will expire in 24 hours. +

+

+ If you didn't create an account with OpenML, you can safely ignore this email. +

+ `; + + return baseLayout(content, logoUrl); +}; diff --git a/app-next/src/lib/email-templates/dataset-edit.ts b/app-next/src/lib/email-templates/dataset-edit.ts new file mode 100644 index 00000000..6da6db8c --- /dev/null +++ b/app-next/src/lib/email-templates/dataset-edit.ts @@ -0,0 +1,48 @@ +// Email specific components +import { baseLayout } from "./layout"; + +export const generateDatasetEditEmail = ( + userName: string, + datasetId: string, + datasetName: string, + changes: string[], + logoUrl: string, + datasetUrl?: string, +) => { + const datasetLink = datasetUrl ?? `https://www.openml.org/datasets/${datasetId}`; + + const content = ` + +

Dataset Updated

+ +
+

Dataset Details

+ + + + + + + + + +
User:${userName}
Dataset:${datasetName} (ID: ${datasetId})
+ +

Changes Made:

+ + + +
+ View Dataset +
+
+ +

+ This is an automated notification for OpenML Admins/Owners. +

+ `; + + return baseLayout(content, logoUrl); +}; diff --git a/app-next/src/lib/email-templates/entity-created.ts b/app-next/src/lib/email-templates/entity-created.ts new file mode 100644 index 00000000..00cfa4be --- /dev/null +++ b/app-next/src/lib/email-templates/entity-created.ts @@ -0,0 +1,51 @@ +import { baseLayout } from "./layout"; + +type EntityType = "dataset" | "task" | "collection"; + +const ENTITY_LABELS: Record = { + dataset: "Dataset", + task: "Task", + collection: "Collection", +}; + +export const generateEntityCreatedEmail = ( + entityType: EntityType, + entityName: string, + entityId: string, + entityUrl: string, + logoUrl: string, +) => { + const label = ENTITY_LABELS[entityType]; + + const content = ` +

${label} Created Successfully

+ +
+

${label} Details

+ + + + + + + + + +
${label}:${entityName}
ID:${entityId}
+ +

+ Your ${label.toLowerCase()} has been submitted to OpenML and will be available shortly. +

+ +
+ View ${label} +
+
+ +

+ This is an automated confirmation from OpenML. +

+ `; + + return baseLayout(content, logoUrl); +}; diff --git a/app-next/src/lib/email-templates/layout.ts b/app-next/src/lib/email-templates/layout.ts new file mode 100644 index 00000000..fdcfa3a0 --- /dev/null +++ b/app-next/src/lib/email-templates/layout.ts @@ -0,0 +1,64 @@ +export const baseLayout = (content: string, logoUrl: string) => ` + + + + + + OpenML Email + + + +
+ + +
+ OpenML Logo +
+ + +
+ ${content} + + +
+

+ Need help? Contact us at openmachinelearning@gmail.com +

+

Best regards,

+

The OpenML Team

+
+
+ + +
+

© ${new Date().getFullYear()} OpenML. Open Machine Learning Platform.

+
+ +
+ + +`; diff --git a/app-next/src/lib/email-templates/profile-update.ts b/app-next/src/lib/email-templates/profile-update.ts new file mode 100644 index 00000000..fb6be680 --- /dev/null +++ b/app-next/src/lib/email-templates/profile-update.ts @@ -0,0 +1,37 @@ +// Email specific components +import { baseLayout } from "./layout"; + +export const generateProfileUpdateEmail = ( + name: string, + fields: string[], + logoUrl: string, +) => { + const content = ` + +

Security Alert: Profile Updated

+ +

Hi ${name},

+

+ This email is to notify you that your OpenML profile was recently updated. +

+ + +
+

The following items were changed:

+ +
+ +

+ If you made these changes, you can safely ignore this email. +

+ + +
+

🔒 Was this not you? Please reset your password immediately or contact support.

+
+ `; + + return baseLayout(content, logoUrl); +}; diff --git a/app-next/src/lib/email-templates/reset-password.ts b/app-next/src/lib/email-templates/reset-password.ts new file mode 100644 index 00000000..ba501fa9 --- /dev/null +++ b/app-next/src/lib/email-templates/reset-password.ts @@ -0,0 +1,35 @@ +import { baseLayout } from "./layout"; + +export const generateResetPasswordEmail = (link: string, logoUrl: string) => { + const content = ` + +

Reset Your Password

+ +

Hi there,

+

We received a request to reset your password for your OpenML account.

+

Click the button below to create a new password:

+ + +
+ Reset Password +
+ + + +
+

Or copy and paste this link into your browser:

+

${link}

+
+ +

+ ⏱️ This reset link will expire in 1 hour. +

+ + +
+

🔒 Security Notice: If you didn't request a password reset, please ignore this email. Your password will remain unchanged.

+
+ `; + + return baseLayout(content, logoUrl); +}; diff --git a/app-next/src/lib/mail.ts b/app-next/src/lib/mail.ts index f5d9619b..fd321929 100644 --- a/app-next/src/lib/mail.ts +++ b/app-next/src/lib/mail.ts @@ -1,9 +1,13 @@ import nodemailer from "nodemailer"; +import { APP_CONFIG } from "@/lib/config"; +import { generateConfirmEmail } from "./email-templates/confirm-email"; +import { generateResetPasswordEmail } from "./email-templates/reset-password"; +import { generateProfileUpdateEmail } from "./email-templates/profile-update"; +import { generateAdminReviewEmail } from "./email-templates/admin-upload"; +import { generateDatasetEditEmail } from "./email-templates/dataset-edit"; +import { generateEntityCreatedEmail } from "./email-templates/entity-created"; -/** - * Shared email utility for sending system emails - */ - +// Shared email utility for sending system emails const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST || "localhost", port: parseInt(process.env.SMTP_PORT || "1025"), @@ -17,60 +21,19 @@ const transporter = nodemailer.createTransport({ : undefined, }); -/** - * Send account confirmation email - */ +// Send account confirmation email export async function sendConfirmationEmail(email: string, token: string) { - const baseUrl = process.env.NEXT_PUBLIC_URL || "http://localhost:3050"; + const baseUrl = APP_CONFIG.siteUrl || "http://localhost:3050"; const link = `${baseUrl}/auth/confirm-email?token=${token}`; const logoUrl = `${baseUrl}/logo_openML_light-bkg.png`; + const htmlContent = generateConfirmEmail(link, logoUrl); + const mailOptions = { from: process.env.SMTP_FROM || '"OpenML" ', to: email, subject: "Welcome to OpenML!", - html: ` -
- -
- OpenML Logo -

Welcome to OpenML!

-
- - -
-

Hi there,

-

Thank you for signing up to OpenML, the collaborative machine learning platform.

-

To verify your email address and activate your account, please click the button below:

- - -
- Confirm Email Address -
- - -
-

Or copy and paste this link into your browser:

-

${link}

-
- -

⏱️ This confirmation link will expire in 24 hours.

-

If you didn't create an account with OpenML, you can safely ignore this email.

- - -
-

Need help? Contact us at openmachinelearning@gmail.com

-

Have a great day,

-

The OpenML Team

-
-
- - -
-

© ${new Date().getFullYear()} OpenML. Open Machine Learning Platform.

-
-
- `, + html: htmlContent, text: `Hi, Thank you for signing up to OpenML. @@ -95,64 +58,19 @@ The OpenML team`, } } -/** - * Send password reset email - */ +//Send password reset email export async function sendPasswordResetEmail(email: string, token: string) { - const baseUrl = process.env.NEXT_PUBLIC_URL || "http://localhost:3050"; + const baseUrl = APP_CONFIG.siteUrl || "http://localhost:3050"; const link = `${baseUrl}/auth/reset-password?token=${token}`; const logoUrl = `${baseUrl}/logo_openML_light-bkg.png`; + const htmlContent = generateResetPasswordEmail(link, logoUrl); + const mailOptions = { from: process.env.SMTP_FROM || '"OpenML" ', to: email, subject: "Reset Your OpenML Password", - html: ` -
- -
- OpenML Logo -

Reset Your Password

-
- - -
-

Hi there,

-

We received a request to reset your password for your OpenML account.

-

Click the button below to create a new password:

- - - - - -
-

Or copy and paste this link into your browser:

-

${link}

-
- -

⏱️ This reset link will expire in 1 hour.

- - -
-

🔒 Security Notice: If you didn't request a password reset, please ignore this email. Your password will remain unchanged.

-
- - -
-

Need help? Contact us at openmachinelearning@gmail.com

-

Best regards,

-

The OpenML Team

-
-
- - -
-

© ${new Date().getFullYear()} OpenML. Open Machine Learning Platform.

-
-
- `, + html: htmlContent, text: `Hi, We received a request to reset your password for your OpenML account. @@ -179,3 +97,171 @@ The OpenML Team`, return { success: false, error }; } } + +// Send profile update notification +export async function sendProfileUpdateEmail(email: string, name: string) { + const baseUrl = APP_CONFIG.siteUrl || "http://localhost:3000"; + const logoUrl = `${baseUrl}/logo_openML_light-bkg.png`; + const supportUrl = "mailto:openmachinelearning@gmail.com"; + + const htmlContent = generateProfileUpdateEmail( + name, + ["Profile Details"], + logoUrl, + ); + + const mailOptions = { + from: process.env.SMTP_FROM || '"OpenML" ', + to: email, + subject: "Security Alert: Profile Updated", + html: htmlContent, + text: `Hi ${name}, + +Your OpenML profile has been updated. + +If you made this change, you can safely ignore this email. +If you did not authorize this change, please contact us immediately at ${supportUrl}. + +Best regards, +The OpenML Team`, + }; + + try { + const info = await transporter.sendMail(mailOptions); + console.log("Profile update email sent:", info.messageId); + return { success: true }; + } catch (error) { + console.error("Error sending profile update email:", error); + return { success: false, error }; + } +} + +//Send dataset upload notification to admins +export async function sendDatasetUploadEmail( + uploaderName: string, + datasetName: string, + datasetId: string | number, +) { + const baseUrl = APP_CONFIG.siteUrl || "http://localhost:3000"; + const datasetUrl = `${baseUrl}/datasets/${datasetId}`; + const logoUrl = `${baseUrl}/logo_openML_light-bkg.png`; + + const htmlContent = generateAdminReviewEmail( + uploaderName, + datasetId.toString(), + datasetName, + logoUrl, + datasetUrl, + ); + + // Send to ADMIN_EMAIL or default support email + const adminEmail = process.env.ADMIN_EMAIL || "openmachinelearning@gmail.com"; + + const mailOptions = { + from: process.env.SMTP_FROM || '"OpenML" ', + to: adminEmail, + subject: `New Dataset Upload: ${datasetName}`, + html: htmlContent, + text: `New Dataset Upload + +User: ${uploaderName} +Dataset: ${datasetName} +Link: ${datasetUrl} + +Please review this dataset to ensure it meets our quality standards.`, + }; + + try { + const info = await transporter.sendMail(mailOptions); + console.log("Dataset upload email sent:", info.messageId); + return { success: true }; + } catch (error) { + console.error("Error sending dataset upload email:", error); + return { success: false, error }; + } +} + +// Send creation confirmation to the creator (dataset, task, or collection) +export async function sendCreationConfirmationEmail( + recipientEmail: string, + entityType: "dataset" | "task" | "collection", + entityName: string, + entityId: string | number, +) { + const baseUrl = APP_CONFIG.siteUrl || "http://localhost:3050"; + const logoUrl = `${baseUrl}/logo_openML_light-bkg.png`; + const pathMap = { dataset: "datasets", task: "tasks", collection: "collections" }; + const entityUrl = `${baseUrl}/${pathMap[entityType]}/${entityId}`; + + const htmlContent = generateEntityCreatedEmail( + entityType, + entityName, + entityId.toString(), + entityUrl, + logoUrl, + ); + + const labelMap = { dataset: "Dataset", task: "Task", collection: "Collection" }; + const label = labelMap[entityType]; + + const mailOptions = { + from: process.env.SMTP_FROM || '"OpenML" ', + to: recipientEmail, + subject: `Your OpenML ${label} "${entityName}" has been created`, + html: htmlContent, + text: `Your ${label} "${entityName}" has been successfully submitted to OpenML.\nView it at: ${entityUrl}\n\nBest regards,\nThe OpenML Team`, + }; + + try { + const info = await transporter.sendMail(mailOptions); + console.log(`${label} creation email sent:`, info.messageId); + return { success: true }; + } catch (error) { + console.error(`Error sending ${label} creation email:`, error); + return { success: false, error }; + } +} + +// Send dataset edit notification + +export async function sendDatasetEditEmail( + recipientEmail: string, + datasetName: string, + datasetId: string | number, +) { + const baseUrl = APP_CONFIG.siteUrl || "http://localhost:3000"; + const datasetUrl = `${baseUrl}/datasets/${datasetId}`; + const logoUrl = `${baseUrl}/logo_openML_light-bkg.png`; + + const htmlContent = generateDatasetEditEmail( + "User", + datasetId.toString(), + datasetName, + ["Metadata updated"], // changes + logoUrl, + datasetUrl, + ); + + const mailOptions = { + from: process.env.SMTP_FROM || '"OpenML" ', + to: recipientEmail, + subject: `Update: Dataset "${datasetName}" has been modified`, + html: htmlContent, + text: `Dataset Update + +The dataset "${datasetName}" has been updated. +View the changes here: ${datasetUrl} + +Best regards, +The OpenML Team`, + }; + + try { + const info = await transporter.sendMail(mailOptions); + console.log("Dataset edit email sent:", info.messageId); + return { success: true }; + } catch (error) { + console.error("Error sending dataset edit email:", error); + return { success: false, error }; + } +}