From 0314add130cd901c25647b873ed042b376af589b Mon Sep 17 00:00:00 2001 From: chark1es Date: Sun, 9 Mar 2025 00:24:09 -0800 Subject: [PATCH] added password resetting and custom passwords --- .../SettingsSection/EmailRequestSettings.tsx | 389 ++++++++++++++++-- src/pages/api/README-password-change.md | 149 ------- src/pages/api/create-ieee-email.ts | 105 ++++- src/pages/api/reset-email-password.ts | 262 ++++++++++++ 4 files changed, 696 insertions(+), 209 deletions(-) delete mode 100644 src/pages/api/README-password-change.md create mode 100644 src/pages/api/reset-email-password.ts diff --git a/src/components/dashboard/SettingsSection/EmailRequestSettings.tsx b/src/components/dashboard/SettingsSection/EmailRequestSettings.tsx index 5e8fc32..6d3593f 100644 --- a/src/components/dashboard/SettingsSection/EmailRequestSettings.tsx +++ b/src/components/dashboard/SettingsSection/EmailRequestSettings.tsx @@ -1,6 +1,5 @@ import { useState, useEffect } from 'react'; import { Authentication } from '../../../scripts/pocketbase/Authentication'; -import { Update } from '../../../scripts/pocketbase/Update'; import { Collections, type User } from '../../../schemas/pocketbase/schema'; import { toast } from 'react-hot-toast'; @@ -9,8 +8,19 @@ export default function EmailRequestSettings() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [requesting, setRequesting] = useState(false); + const [resettingPassword, setResettingPassword] = useState(false); const [isOfficer, setIsOfficer] = useState(false); const [createdEmail, setCreatedEmail] = useState(null); + const [showPasswordReset, setShowPasswordReset] = useState(false); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [passwordError, setPasswordError] = useState(''); + + // For initial email creation + const [initialPassword, setInitialPassword] = useState(''); + const [initialConfirmPassword, setInitialConfirmPassword] = useState(''); + const [initialPasswordError, setInitialPasswordError] = useState(''); + const [showEmailForm, setShowEmailForm] = useState(false); useEffect(() => { const loadUserData = async () => { @@ -46,12 +56,43 @@ export default function EmailRequestSettings() { loadUserData(); }, []); + const toggleEmailForm = () => { + setShowEmailForm(!showEmailForm); + setInitialPassword(''); + setInitialConfirmPassword(''); + setInitialPasswordError(''); + }; + + const validateInitialPassword = () => { + if (initialPassword.length < 8) { + setInitialPasswordError('Password must be at least 8 characters long'); + return false; + } + + if (initialPassword !== initialConfirmPassword) { + setInitialPasswordError('Passwords do not match'); + return false; + } + + setInitialPasswordError(''); + return true; + }; + const handleRequestEmail = async () => { if (!user) return; + if (initialPassword && !validateInitialPassword()) { + return; + } + try { setRequesting(true); + // Determine what the email will be + const emailUsername = user.email.split('@')[0].toLowerCase().replace(/[^a-z0-9]/g, ""); + const emailDomain = import.meta.env.PUBLIC_MXROUTE_EMAIL_DOMAIN || 'ieeeucsd.org'; + const expectedEmail = `${emailUsername}@${emailDomain}`; + // Call the API to create the email account const response = await fetch('/api/create-ieee-email', { method: 'POST', @@ -61,7 +102,8 @@ export default function EmailRequestSettings() { body: JSON.stringify({ userId: user.id, name: user.name, - email: user.email + email: user.email, + password: initialPassword || undefined }) }); @@ -77,7 +119,8 @@ export default function EmailRequestSettings() { requested_email: true }); - toast.success('IEEE email created successfully! Check your email for login details.'); + toast.success('IEEE email created successfully!'); + setShowEmailForm(false); } else { toast.error(result.message || 'Failed to create email. Please contact the webmaster for assistance.'); } @@ -89,6 +132,71 @@ export default function EmailRequestSettings() { } }; + const togglePasswordReset = () => { + setShowPasswordReset(!showPasswordReset); + setNewPassword(''); + setConfirmPassword(''); + setPasswordError(''); + }; + + const validatePassword = () => { + if (newPassword.length < 8) { + setPasswordError('Password must be at least 8 characters long'); + return false; + } + + if (newPassword !== confirmPassword) { + setPasswordError('Passwords do not match'); + return false; + } + + setPasswordError(''); + return true; + }; + + const handleResetPassword = async () => { + if (!user || !user.requested_email) return; + + if (!validatePassword()) { + return; + } + + // Determine the email address + const emailAddress = createdEmail || (user ? `${user.email.split('@')[0].toLowerCase().replace(/[^a-z0-9]/g, "")}@ieeeucsd.org` : ''); + + try { + setResettingPassword(true); + + // Call the API to reset the password + const response = await fetch('/api/reset-email-password', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email: emailAddress, + password: newPassword + }) + }); + + const result = await response.json(); + + if (response.ok && result.success) { + toast.success('Password reset successfully!'); + setShowPasswordReset(false); + setNewPassword(''); + setConfirmPassword(''); + } else { + toast.error(result.message || 'Failed to reset password. Please contact the webmaster for assistance.'); + } + } catch (error) { + console.error('Error resetting password:', error); + toast.error('Failed to reset password. Please try again later.'); + } finally { + setResettingPassword(false); + } + }; + if (loading) { return (
@@ -107,27 +215,154 @@ export default function EmailRequestSettings() { if (user?.requested_email || createdEmail) { return ( -
+

{createdEmail ? 'Your IEEE Email Address' : 'Email Request Status'}

- {createdEmail && ( -
-

{createdEmail}

+
+

+ {createdEmail || (user ? `${user.email.split('@')[0].toLowerCase().replace(/[^a-z0-9]/g, "")}@ieeeucsd.org` : '')} +

+ {initialPassword ? ( +

Your email has been created with the password you provided.

+ ) : (

Check your personal email for login instructions.

-
- )} + )} +

Access Your Email

+
+ {!showPasswordReset ? ( + + ) : ( +
+

Reset Your Email Password

+ +
+ + + Important: After resetting your password, you'll need to update it in any email clients, Gmail integrations, or mobile devices where you've set up this email account. + +
+ +
+ + setNewPassword(e.target.value)} + placeholder="Enter new password" + /> +
+ +
+ + setConfirmPassword(e.target.value)} + placeholder="Confirm new password" + /> +
+ + {passwordError && ( +
{passwordError}
+ )} + +
+ + +
+
+ )} + + {!showPasswordReset && ( +

+ Reset your IEEE email password to a new password of your choice. +

+ )} +
+
+ +
+

Setting Up Your IEEE Email in Gmail

+ +
+

First Step: Set Up Sending From Your IEEE Email

+
    +
  1. Go to settings (gear icon) → Accounts and Import
  2. +
  3. In the section that says Send mail as:, select Reply from the same address the message was sent to
  4. +
  5. In that same section, select Add another email address
  6. +
  7. For the Name, put your actual name (e.g. Charles Nguyen) if this is your personal ieeeucsd.org or put the department name (e.g. IEEEUCSD Webmaster)
  8. +
  9. For the Email address, put the email that was provided for you
  10. +
  11. Make sure the Treat as an alias button is selected. Go to the next step
  12. +
  13. For the SMTP Server, put mail.ieeeucsd.org
  14. +
  15. For the username, put in your FULL ieeeucsd email address
  16. +
  17. For the password, put in the email's password
  18. +
  19. For the port, put in 587
  20. +
  21. Make sure you select Secured connection with TLS
  22. +
  23. Go back to mail.ieeeucsd.org and verify the email that Google has sent you
  24. +
+
+ +
+

Second Step: Set Up Receiving Your IEEE Email

+
    +
  1. Go to settings (gear icon) → Accounts and Import
  2. +
  3. In the section that says Check mail from other accounts:, select Add a mail account
  4. +
  5. Put in the ieeeucsd email and hit next
  6. +
  7. Make sure Import emails from my other account (POP3) is selected, then hit next
  8. +
  9. For the username, put in your full ieeeucsd.org email
  10. +
  11. For the password, put in your ieeeucsd.org password
  12. +
  13. For the POP Server, put in mail.ieeeucsd.org
  14. +
  15. For the Port, put in 995
  16. +
  17. Select Leave a copy of retrieved message on the server
  18. +
  19. Select Always use a secure connection (SSL) when retrieving mail
  20. +
  21. Select Label incoming messages
  22. +
  23. Then hit Add Account
  24. +
+
+
+ +

If you have any questions or need help with your IEEE email, please contact webmaster@ieeeucsd.org

@@ -138,39 +373,111 @@ export default function EmailRequestSettings() { return (
-

- As an IEEE officer, you're eligible for an official IEEE UCSD email address. This email can be used for all IEEE-related communications and provides a professional identity when representing the organization. -

+ {!showEmailForm ? ( + <> +

+ As an IEEE officer, you're eligible for an official IEEE UCSD email address. This email can be used for all IEEE-related communications and provides a professional identity when representing the organization. +

-
-

Benefits of an IEEE email:

-
    -
  • Professional communication with sponsors and partners
  • -
  • Consistent branding for IEEE UCSD
  • -
  • Separation between personal and IEEE communications
  • -
  • Access to IEEE UCSD shared resources
  • -
-
+
+

Benefits of an IEEE email:

+
    +
  • Professional communication with sponsors and partners
  • +
  • Consistent branding for IEEE UCSD
  • +
  • Separation between personal and IEEE communications
  • +
  • Access to IEEE UCSD shared resources
  • +
+
- +
+

Your IEEE Email Address

+

When you request an email, you'll receive:

+

+ {user?.email ? `${user.email.split('@')[0].toLowerCase().replace(/[^a-z0-9]/g, "")}@ieeeucsd.org` : 'Loading...'} +

+
-
-

By requesting an email, you agree to use it responsibly and in accordance with IEEE UCSD policies.

-

Your email address will be based on your current email username.

-
+ + +
+

By requesting an email, you agree to use it responsibly and in accordance with IEEE UCSD policies.

+
+ + ) : ( +
+

Create Your IEEE Email

+ +
+

Your email address will be:

+

+ {user?.email ? `${user.email.split('@')[0].toLowerCase().replace(/[^a-z0-9]/g, "")}@ieeeucsd.org` : 'Loading...'} +

+
+ +
+ + setInitialPassword(e.target.value)} + placeholder="Enter password (min. 8 characters)" + /> +
+ +
+ + setInitialConfirmPassword(e.target.value)} + placeholder="Confirm password" + /> +
+ + {initialPasswordError && ( +
{initialPasswordError}
+ )} + +

+ Leave the password fields empty if you want a secure random password to be generated and sent to your personal email. +

+ +
+ + +
+
+ )}
); } \ No newline at end of file diff --git a/src/pages/api/README-password-change.md b/src/pages/api/README-password-change.md deleted file mode 100644 index 6cafdd7..0000000 --- a/src/pages/api/README-password-change.md +++ /dev/null @@ -1,149 +0,0 @@ -# LogTo Password Change Implementation - -This document explains how the password change functionality works with LogTo authentication. - -## Overview - -The password change functionality uses LogTo's Management API to update user passwords. The implementation follows the Machine-to-Machine (M2M) authentication flow as described in the LogTo documentation. - -## Key Files - -1. **`/src/pages/api/change-password.ts`**: The server-side API endpoint that handles password change requests -2. **`/src/components/dashboard/SettingsSection/PasswordChangeSettings.tsx`**: The React component that provides the password change UI -3. **`/src/components/dashboard/SettingsSection/AccountSecuritySettings.tsx`**: The parent component that includes the password change functionality - -## How It Works - -### Authentication Flow - -1. The client sends a password change request with the user ID and new password -2. The server obtains an access token using the client credentials flow -3. The server uses the access token to make an authenticated request to the LogTo Management API -4. The LogTo API updates the user's password - -### Implementation Details - -#### 1. Client Credentials Flow - -The implementation tries multiple approaches to obtain an access token using the OAuth 2.0 client credentials flow: - -```javascript -// Attempt 1: Without resource parameter -let tokenResponse = await fetch(logtoTokenEndpoint, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - grant_type: "client_credentials", - client_id: logtoAppId, - client_secret: logtoAppSecret, - scope: "all", - }).toString(), -}); - -// Attempt 2: With Basic Auth -tokenResponse = await fetch(logtoTokenEndpoint, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Authorization: `Basic ${Buffer.from(`${logtoAppId}:${logtoAppSecret}`).toString("base64")}`, - }, - body: new URLSearchParams({ - grant_type: "client_credentials", - scope: "all", - }).toString(), -}); - -// Attempt 3: With organization_id -tokenResponse = await fetch(logtoTokenEndpoint, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - grant_type: "client_credentials", - client_id: logtoAppId, - client_secret: logtoAppSecret, - organization_id: "default", - scope: "all", - }).toString(), -}); - -// Attempt 4: With audience parameter -tokenResponse = await fetch(logtoTokenEndpoint, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - grant_type: "client_credentials", - client_id: logtoAppId, - client_secret: logtoAppSecret, - audience: "https://auth.ieeeucsd.org/api", - scope: "all", - }).toString(), -}); -``` - -Key points: - -- The `grant_type` must be `client_credentials` -- Multiple approaches are tried to handle different LogTo configurations -- The `scope` parameter is set to `all` to request all available permissions - -#### 2. Password Update API - -After obtaining an access token, the implementation calls the LogTo Management API to update the password: - -```javascript -const passwordEndpoint = `${logtoApiEndpoint}/api/users/${userId}/password`; - -const changePasswordResponse = await fetch(passwordEndpoint, { - method: "PATCH", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - password: newPassword, - }), -}); -``` - -Key points: - -- The endpoint is `/api/users/{userId}/password` -- The HTTP method is `PATCH` -- The request body contains only the `password` field -- The `Authorization` header must include the access token - -## Troubleshooting - -### Common Issues - -1. **Authentication Errors (401)** - - - Check that the client ID and secret are correct - - Verify that the M2M application has the necessary permissions - -2. **Permission Errors (403)** - - - Ensure the M2M application has been assigned the appropriate role - - Check that the role has the necessary permissions for user management - -3. **Resource Parameter Issues** - - - The implementation tries multiple approaches to handle resource parameter issues - - Different LogTo configurations may require different parameters (resource, audience, etc.) - -4. **User ID Issues** - - Ensure the user ID is correctly retrieved from the authentication system - - The user ID should match the LogTo user ID, not the local user ID - -## References - -- [LogTo M2M Quick Start](https://docs.logto.io/quick-starts/m2m) -- [LogTo API Reference](https://openapi.logto.io/) -- [LogTo Update User Password API](https://openapi.logto.io/operation/operation-updateuserpassword) -- [LogTo Authentication](https://openapi.logto.io/authentication) diff --git a/src/pages/api/create-ieee-email.ts b/src/pages/api/create-ieee-email.ts index f043698..0e96709 100644 --- a/src/pages/api/create-ieee-email.ts +++ b/src/pages/api/create-ieee-email.ts @@ -2,9 +2,23 @@ import type { APIRoute } from "astro"; export const POST: APIRoute = async ({ request }) => { try { - const { userId, name, email } = await request.json(); + console.log("Email creation request received"); + + const requestBody = await request.json(); + console.log( + "Request body:", + JSON.stringify({ + userId: requestBody.userId, + name: requestBody.name, + email: requestBody.email, + passwordProvided: !!requestBody.password, + }), + ); + + const { userId, name, email, password } = requestBody; if (!userId || !name || !email) { + console.log("Missing required parameters"); return new Response( JSON.stringify({ success: false, @@ -21,12 +35,15 @@ export const POST: APIRoute = async ({ request }) => { // Extract username from email (everything before the @ symbol) const emailUsername = email.split("@")[0].toLowerCase(); + console.log(`Email username extracted: ${emailUsername}`); // Remove any special characters that might cause issues const cleanUsername = emailUsername.replace(/[^a-z0-9]/g, ""); + console.log(`Cleaned username: ${cleanUsername}`); - // Generate a secure random password - const password = generateSecurePassword(); + // Use provided password or generate a secure random one if not provided + const newPassword = password || generateSecurePassword(); + console.log(`Using ${password ? "user-provided" : "generated"} password`); // MXRoute DirectAdmin API credentials from environment variables const loginKey = import.meta.env.MXROUTE_LOGIN_KEY; @@ -36,12 +53,20 @@ export const POST: APIRoute = async ({ request }) => { const emailOutboundLimit = import.meta.env.MXROUTE_EMAIL_OUTBOUND_LIMIT; const emailDomain = import.meta.env.MXROUTE_EMAIL_DOMAIN; + console.log(`Environment variables: + loginKey: ${loginKey ? "Set" : "Not set"} + serverLogin: ${serverLogin ? "Set" : "Not set"} + serverUrl: ${serverUrl ? "Set" : "Not set"} + emailQuota: ${emailQuota || "Not set"} + emailOutboundLimit: ${emailOutboundLimit || "Not set"} + emailDomain: ${emailDomain || "Not set"} + `); + if (!loginKey || !serverLogin || !serverUrl || !emailDomain) { throw new Error("Missing MXRoute configuration"); } // DirectAdmin API endpoint for creating email accounts - // According to the documentation: https://docs.directadmin.com/developer/api/legacy-api.html let baseUrl = serverUrl; // If the URL contains a specific command, extract just the base URL @@ -65,17 +90,22 @@ export const POST: APIRoute = async ({ request }) => { formData.append("action", "create"); formData.append("domain", emailDomain); formData.append("user", cleanUsername); // DirectAdmin uses 'user' for POP accounts - formData.append("passwd", password); - formData.append("passwd2", password); + formData.append("passwd", newPassword); + formData.append("passwd2", newPassword); formData.append("quota", emailQuota || "200"); formData.append("limit", emailOutboundLimit || "9600"); - // Log the form data being sent + // Log the form data being sent (without showing the actual password) console.log("Form data:"); - formData.forEach((value, key) => { - console.log(` ${key}: ${value}`); - }); + console.log(` action: create`); + console.log(` domain: ${emailDomain}`); + console.log(` user: ${cleanUsername}`); + console.log(` passwd: ********`); + console.log(` passwd2: ********`); + console.log(` quota: ${emailQuota || "200"}`); + console.log(` limit: ${emailOutboundLimit || "9600"}`); + console.log("Sending request to DirectAdmin API..."); const response = await fetch(emailApiUrl, { method: "POST", headers: { @@ -86,6 +116,7 @@ export const POST: APIRoute = async ({ request }) => { }); const responseText = await response.text(); + console.log(`DirectAdmin response status: ${response.status}`); console.log(`DirectAdmin response: ${responseText}`); // DirectAdmin API returns "error=1" in the response text for errors @@ -126,14 +157,19 @@ export const POST: APIRoute = async ({ request }) => { throw new Error(errorMessage); } - // Send notification email to the user with their new email credentials - await sendCredentialsEmail( - email, - `${cleanUsername}@${emailDomain}`, - password, - ); + console.log("Email account created successfully"); - // Send notification to webmaster + // Only send notification email if we generated a random password + if (!password) { + console.log("Sending credentials email to user"); + await sendCredentialsEmail( + email, + `${cleanUsername}@${emailDomain}`, + newPassword, + ); + } + + console.log("Sending notification to webmaster"); await sendWebmasterNotification( userId, name, @@ -146,8 +182,9 @@ export const POST: APIRoute = async ({ request }) => { success: true, data: { ieeeEmail: `${cleanUsername}@${emailDomain}`, - message: - "Email account created successfully. Check your email for login details.", + message: password + ? "Email account created successfully with your chosen password." + : "Email account created successfully. Check your email for login details.", }, }), { @@ -223,6 +260,36 @@ async function sendCredentialsEmail( - Webmail: https://heracles.mxrouting.net:2096/ - IMAP/SMTP settings can be found at: https://mxroute.com/setup/ + ===== Setting Up Your IEEE Email in Gmail ===== + + --- First Step: Set Up Sending From Your IEEE Email --- + 1. Go to settings (gear icon) → Accounts and Import + 2. In the section that says "Send mail as:", select "Reply from the same address the message was sent to" + 3. In that same section, select "Add another email address" + 4. For the Name, put your actual name or department name (e.g. IEEEUCSD Webmaster) + 5. For the Email address, put ${ieeeEmail} + 6. Make sure the "Treat as an alias" button is selected. Go to the next step + 7. For the SMTP Server, put mail.ieeeucsd.org + 8. For the username, put in your FULL ieeeucsd email address (${ieeeEmail}) + 9. For the password, put in the email's password (provided above) + 10. For the port, put in 587 + 11. Make sure you select "Secured connection with TLS" + 12. Go back to mail.ieeeucsd.org and verify the email that Google has sent you + + --- Second Step: Set Up Receiving Your IEEE Email --- + 1. Go to settings (gear icon) → Accounts and Import + 2. In the section that says "Check mail from other accounts:", select "Add a mail account" + 3. Put in ${ieeeEmail} and hit next + 4. Make sure "Import emails from my other account (POP3)" is selected, then hit next + 5. For the username, put in ${ieeeEmail} + 6. For the password, put in your password (provided above) + 7. For the POP Server, put in mail.ieeeucsd.org + 8. For the Port, put in 995 + 9. Select "Leave a copy of retrieved message on the server" + 10. Select "Always use a secure connection (SSL) when retrieving mail" + 11. Select "Label incoming messages" + 12. Then hit "Add Account" + Please change your password after your first login. If you have any questions, please contact webmaster@ieeeucsd.org. diff --git a/src/pages/api/reset-email-password.ts b/src/pages/api/reset-email-password.ts new file mode 100644 index 0000000..f6048b7 --- /dev/null +++ b/src/pages/api/reset-email-password.ts @@ -0,0 +1,262 @@ +import type { APIRoute } from "astro"; + +export const POST: APIRoute = async ({ request }) => { + try { + console.log("Password reset request received"); + + const requestBody = await request.json(); + console.log( + "Request body:", + JSON.stringify({ + email: requestBody.email, + passwordProvided: !!requestBody.password, + }), + ); + + const { email, password } = requestBody; + + if (!email) { + console.log("Missing email address"); + return new Response( + JSON.stringify({ + success: false, + message: "Missing email address", + }), + { + status: 400, + headers: { + "Content-Type": "application/json", + }, + }, + ); + } + + // Extract username and domain from email + const [username, domain] = email.split("@"); + console.log(`Email parsed: username=${username}, domain=${domain}`); + + if (!username || !domain) { + console.log("Invalid email format"); + return new Response( + JSON.stringify({ + success: false, + message: "Invalid email format", + }), + { + status: 400, + headers: { + "Content-Type": "application/json", + }, + }, + ); + } + + // Use provided password or generate a secure random one if not provided + const newPassword = password || generateSecurePassword(); + console.log(`Using ${password ? "user-provided" : "generated"} password`); + + // MXRoute DirectAdmin API credentials from environment variables + const loginKey = import.meta.env.MXROUTE_LOGIN_KEY; + const serverLogin = import.meta.env.MXROUTE_SERVER_LOGIN; + const serverUrl = import.meta.env.MXROUTE_SERVER_URL; + + console.log(`Environment variables: + loginKey: ${loginKey ? "Set" : "Not set"} + serverLogin: ${serverLogin ? "Set" : "Not set"} + serverUrl: ${serverUrl ? "Set" : "Not set"} + `); + + if (!loginKey || !serverLogin || !serverUrl) { + throw new Error("Missing MXRoute configuration"); + } + + // DirectAdmin API endpoint for changing email password + let baseUrl = serverUrl; + + // If the URL contains a specific command, extract just the base URL + if (baseUrl.includes("/CMD_")) { + baseUrl = baseUrl.split("/CMD_")[0]; + } + + // Make sure there's no trailing slash + baseUrl = baseUrl.replace(/\/$/, ""); + + // Construct the email POP API URL + const emailApiUrl = `${baseUrl}/CMD_API_EMAIL_POP`; + + console.log(`Resetting password for email: ${email}`); + console.log(`DirectAdmin API URL: ${emailApiUrl}`); + + // Create the form data for password reset + const formData = new URLSearchParams(); + formData.append("action", "modify"); + formData.append("domain", domain); + formData.append("user", username); + formData.append("passwd", newPassword); + formData.append("passwd2", newPassword); + + // Log the form data being sent (without showing the actual password) + console.log("Form data:"); + console.log(` action: modify`); + console.log(` domain: ${domain}`); + console.log(` user: ${username}`); + console.log(` passwd: ********`); + console.log(` passwd2: ********`); + + console.log("Sending request to DirectAdmin API..."); + const response = await fetch(emailApiUrl, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Authorization: `Basic ${Buffer.from(`${serverLogin}:${loginKey}`).toString("base64")}`, + }, + body: formData, + }); + + const responseText = await response.text(); + console.log(`DirectAdmin response status: ${response.status}`); + console.log(`DirectAdmin response: ${responseText}`); + + // DirectAdmin API returns "error=1" in the response text for errors + if (responseText.includes("error=1") || !response.ok) { + console.error("Error resetting email password:", responseText); + + // Parse the error details if possible + let errorMessage = "Failed to reset email password"; + try { + const errorParams = new URLSearchParams(responseText); + if (errorParams.has("text")) { + errorMessage = decodeURIComponent(errorParams.get("text") || ""); + } + if (errorParams.has("details")) { + const details = decodeURIComponent(errorParams.get("details") || ""); + errorMessage += `: ${details.replace(/
/g, " ")}`; + } + } catch (e) { + console.error("Error parsing DirectAdmin error response:", e); + } + + throw new Error(errorMessage); + } + + console.log("Password reset successful"); + + // Only send notification email if we generated a random password + if (!password) { + await sendPasswordResetEmail(email, newPassword); + } + + return new Response( + JSON.stringify({ + success: true, + message: password + ? "Password reset successfully. Remember to update your password in any email clients or integrations." + : "Password reset successfully. Check your personal email for the new password.", + }), + { + status: 200, + headers: { + "Content-Type": "application/json", + }, + }, + ); + } catch (error) { + console.error("Error in reset-email-password:", error); + return new Response( + JSON.stringify({ + success: false, + message: error instanceof Error ? error.message : "An error occurred", + }), + { + status: 500, + headers: { + "Content-Type": "application/json", + }, + }, + ); + } +}; + +// Generate a secure random password +function generateSecurePassword(length = 16) { + const charset = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+"; + let password = ""; + + // Ensure at least one character from each category + password += charset.substring(0, 26).charAt(Math.floor(Math.random() * 26)); // lowercase + password += charset.substring(26, 52).charAt(Math.floor(Math.random() * 26)); // uppercase + password += charset.substring(52, 62).charAt(Math.floor(Math.random() * 10)); // number + password += charset + .substring(62) + .charAt(Math.floor(Math.random() * (charset.length - 62))); // special + + // Fill the rest randomly + for (let i = 4; i < length; i++) { + password += charset.charAt(Math.floor(Math.random() * charset.length)); + } + + // Shuffle the password + return password + .split("") + .sort(() => 0.5 - Math.random()) + .join(""); +} + +// Send email with new password +async function sendPasswordResetEmail(ieeeEmail: string, newPassword: string) { + try { + // Extract username from IEEE email + const username = ieeeEmail.split("@")[0]; + + // Get the user from PocketBase to find their personal email + const PocketBase = await import("pocketbase").then( + (module) => module.default, + ); + const pb = new PocketBase( + import.meta.env.POCKETBASE_URL || "http://127.0.0.1:8090", + ); + + // Try to find the user with this username + const userRecord = await pb.collection("users").getList(1, 1, { + filter: `email~"${username}@"`, + }); + + // Determine which email to send to + let recipientEmail = ieeeEmail; + if (userRecord && userRecord.items.length > 0) { + recipientEmail = userRecord.items[0].email; + } + + // In a real implementation, you would use an email service like SendGrid, Mailgun, etc. + // For now, we'll just log the email that would be sent + console.log(` + To: ${recipientEmail} + Subject: Your IEEE UCSD Email Password Has Been Reset + + Hello, + + Your IEEE UCSD email password has been reset: + + IEEE Email address: ${ieeeEmail} + New Password: ${newPassword} + + You can access your email through: + - Webmail: https://mail.ieeeucsd.org + + Please consider changing this password to something you can remember after logging in. + + If you did not request this password reset, please contact webmaster@ieeeucsd.org immediately. + + Best regards, + IEEE UCSD Web Team + `); + + // In a production environment, replace with actual email sending code + return true; + } catch (error) { + console.error("Error sending password reset email:", error); + // Still return true to not block the password reset process + return true; + } +}