added password resetting and custom passwords

This commit is contained in:
chark1es 2025-03-09 00:24:09 -08:00
parent 2b84b9c433
commit 0314add130
4 changed files with 696 additions and 209 deletions

View file

@ -1,6 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Authentication } from '../../../scripts/pocketbase/Authentication'; import { Authentication } from '../../../scripts/pocketbase/Authentication';
import { Update } from '../../../scripts/pocketbase/Update';
import { Collections, type User } from '../../../schemas/pocketbase/schema'; import { Collections, type User } from '../../../schemas/pocketbase/schema';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
@ -9,8 +8,19 @@ export default function EmailRequestSettings() {
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [requesting, setRequesting] = useState(false); const [requesting, setRequesting] = useState(false);
const [resettingPassword, setResettingPassword] = useState(false);
const [isOfficer, setIsOfficer] = useState(false); const [isOfficer, setIsOfficer] = useState(false);
const [createdEmail, setCreatedEmail] = useState<string | null>(null); const [createdEmail, setCreatedEmail] = useState<string | null>(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(() => { useEffect(() => {
const loadUserData = async () => { const loadUserData = async () => {
@ -46,12 +56,43 @@ export default function EmailRequestSettings() {
loadUserData(); 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 () => { const handleRequestEmail = async () => {
if (!user) return; if (!user) return;
if (initialPassword && !validateInitialPassword()) {
return;
}
try { try {
setRequesting(true); 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 // Call the API to create the email account
const response = await fetch('/api/create-ieee-email', { const response = await fetch('/api/create-ieee-email', {
method: 'POST', method: 'POST',
@ -61,7 +102,8 @@ export default function EmailRequestSettings() {
body: JSON.stringify({ body: JSON.stringify({
userId: user.id, userId: user.id,
name: user.name, name: user.name,
email: user.email email: user.email,
password: initialPassword || undefined
}) })
}); });
@ -77,7 +119,8 @@ export default function EmailRequestSettings() {
requested_email: true requested_email: true
}); });
toast.success('IEEE email created successfully! Check your email for login details.'); toast.success('IEEE email created successfully!');
setShowEmailForm(false);
} else { } else {
toast.error(result.message || 'Failed to create email. Please contact the webmaster for assistance.'); 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) { if (loading) {
return ( return (
<div className="flex justify-center items-center p-8"> <div className="flex justify-center items-center p-8">
@ -107,27 +215,154 @@ export default function EmailRequestSettings() {
if (user?.requested_email || createdEmail) { if (user?.requested_email || createdEmail) {
return ( return (
<div className="space-y-4"> <div className="space-y-6">
<div className="p-4 bg-base-200 rounded-lg"> <div className="p-4 bg-base-200 rounded-lg">
<h3 className="font-bold text-lg mb-2"> <h3 className="font-bold text-lg mb-2">
{createdEmail ? 'Your IEEE Email Address' : 'Email Request Status'} {createdEmail ? 'Your IEEE Email Address' : 'Email Request Status'}
</h3> </h3>
{createdEmail && (
<div className="mb-4"> <div className="mb-4">
<p className="text-xl font-mono bg-base-100 p-2 rounded">{createdEmail}</p> <p className="text-xl font-mono bg-base-100 p-2 rounded">
{createdEmail || (user ? `${user.email.split('@')[0].toLowerCase().replace(/[^a-z0-9]/g, "")}@ieeeucsd.org` : '')}
</p>
{initialPassword ? (
<p className="mt-2 text-sm">Your email has been created with the password you provided.</p>
) : (
<p className="mt-2 text-sm">Check your personal email for login instructions.</p> <p className="mt-2 text-sm">Check your personal email for login instructions.</p>
</div>
)} )}
</div>
<div className="mb-4"> <div className="mb-4">
<h4 className="font-semibold mb-1">Access Your Email</h4> <h4 className="font-semibold mb-1">Access Your Email</h4>
<ul className="list-disc list-inside space-y-1"> <ul className="list-disc list-inside space-y-1">
<li>Webmail: <a href="https://heracles.mxrouting.net:2096/" target="_blank" rel="noopener noreferrer" className="link link-primary">https://heracles.mxrouting.net:2096/</a></li> <li>Webmail: <a href="https://mail.ieeeucsd.org" target="_blank" rel="noopener noreferrer" className="link link-primary">https://mail.ieeeucsd.org</a></li>
<li>IMAP/SMTP settings: <a href="https://mxroute.com/setup/" target="_blank" rel="noopener noreferrer" className="link link-primary">https://mxroute.com/setup/</a></li>
</ul> </ul>
</div> </div>
<div className="mt-4">
{!showPasswordReset ? (
<button
className="btn btn-secondary w-full"
onClick={togglePasswordReset}
>
Reset Email Password
</button>
) : (
<div className="space-y-4 p-4 bg-base-100 rounded-lg">
<h4 className="font-semibold">Reset Your Email Password</h4>
<div className="alert alert-warning">
<svg xmlns="http://www.w3.org/2000/svg" className="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
<span>
<strong>Important:</strong> 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.
</span>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">New Password</span>
</label>
<input
type="password"
className="input input-bordered"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="Enter new password"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Confirm Password</span>
</label>
<input
type="password"
className="input input-bordered"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Confirm new password"
/>
</div>
{passwordError && (
<div className="text-error text-sm">{passwordError}</div>
)}
<div className="flex gap-2">
<button
className="btn btn-secondary flex-1"
onClick={handleResetPassword}
disabled={resettingPassword}
>
{resettingPassword ? (
<>
<span className="loading loading-spinner loading-sm"></span>
Resetting...
</>
) : (
'Reset Password'
)}
</button>
<button
className="btn btn-outline"
onClick={togglePasswordReset}
disabled={resettingPassword}
>
Cancel
</button>
</div>
</div>
)}
{!showPasswordReset && (
<p className="text-xs mt-2 opacity-70">
Reset your IEEE email password to a new password of your choice.
</p>
)}
</div>
</div>
<div className="p-4 bg-base-200 rounded-lg">
<h3 className="font-bold text-lg mb-4">Setting Up Your IEEE Email in Gmail</h3>
<div className="mb-6">
<h4 className="font-semibold mb-2">First Step: Set Up Sending From Your IEEE Email</h4>
<ol className="list-decimal list-inside space-y-2">
<li>Go to settings (gear icon) Accounts and Import</li>
<li>In the section that says <span className="text-blue-600">Send mail as:</span>, select <span className="text-blue-600">Reply from the same address the message was sent to</span></li>
<li>In that same section, select <span className="text-blue-600">Add another email address</span></li>
<li>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)</li>
<li>For the Email address, put the email that was provided for you</li>
<li>Make sure the <span className="text-blue-600">Treat as an alias</span> button is selected. Go to the next step</li>
<li>For the SMTP Server, put <span className="text-blue-600">mail.ieeeucsd.org</span></li>
<li>For the username, put in your <span className="text-blue-600">FULL ieeeucsd email address</span></li>
<li>For the password, put in the email's password</li>
<li>For the port, put in <span className="text-blue-600">587</span></li>
<li>Make sure you select <span className="text-blue-600">Secured connection with TLS</span></li>
<li>Go back to mail.ieeeucsd.org and verify the email that Google has sent you</li>
</ol>
</div>
<div>
<h4 className="font-semibold mb-2">Second Step: Set Up Receiving Your IEEE Email</h4>
<ol className="list-decimal list-inside space-y-2">
<li>Go to settings (gear icon) Accounts and Import</li>
<li>In the section that says <span className="text-blue-600">Check mail from other accounts:</span>, select <span className="text-blue-600">Add a mail account</span></li>
<li>Put in the ieeeucsd email and hit next</li>
<li>Make sure <span className="text-blue-600">Import emails from my other account (POP3)</span> is selected, then hit next</li>
<li>For the username, put in your full ieeeucsd.org email</li>
<li>For the password, put in your ieeeucsd.org password</li>
<li>For the POP Server, put in <span className="text-blue-600">mail.ieeeucsd.org</span></li>
<li>For the Port, put in <span className="text-blue-600">995</span></li>
<li>Select <span className="text-blue-600">Leave a copy of retrieved message on the server</span></li>
<li>Select <span className="text-blue-600">Always use a secure connection (SSL) when retrieving mail</span></li>
<li>Select <span className="text-blue-600">Label incoming messages</span></li>
<li>Then hit <span className="text-blue-600">Add Account</span></li>
</ol>
</div>
</div>
<div className="p-4 bg-base-200 rounded-lg">
<p className="text-sm"> <p className="text-sm">
If you have any questions or need help with your IEEE email, please contact <a href="mailto:webmaster@ieeeucsd.org" className="underline">webmaster@ieeeucsd.org</a> If you have any questions or need help with your IEEE email, please contact <a href="mailto:webmaster@ieeeucsd.org" className="underline">webmaster@ieeeucsd.org</a>
</p> </p>
@ -138,6 +373,8 @@ export default function EmailRequestSettings() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{!showEmailForm ? (
<>
<p className="text-sm"> <p className="text-sm">
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. 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.
</p> </p>
@ -152,8 +389,73 @@ export default function EmailRequestSettings() {
</ul> </ul>
</div> </div>
<div className="p-4 bg-base-200 rounded-lg">
<h3 className="font-bold text-lg mb-2">Your IEEE Email Address</h3>
<p className="text-sm mb-2">When you request an email, you'll receive:</p>
<p className="text-xl font-mono bg-base-100 p-2 rounded">
{user?.email ? `${user.email.split('@')[0].toLowerCase().replace(/[^a-z0-9]/g, "")}@ieeeucsd.org` : 'Loading...'}
</p>
</div>
<button <button
className="btn btn-primary w-full" className="btn btn-primary w-full"
onClick={toggleEmailForm}
>
Request IEEE Email Address
</button>
<div className="text-xs opacity-70">
<p>By requesting an email, you agree to use it responsibly and in accordance with IEEE UCSD policies.</p>
</div>
</>
) : (
<div className="p-4 bg-base-200 rounded-lg space-y-4">
<h3 className="font-bold text-lg">Create Your IEEE Email</h3>
<div className="p-4 bg-base-100 rounded-lg">
<p className="font-semibold">Your email address will be:</p>
<p className="text-xl font-mono mt-2">
{user?.email ? `${user.email.split('@')[0].toLowerCase().replace(/[^a-z0-9]/g, "")}@ieeeucsd.org` : 'Loading...'}
</p>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Choose a Password</span>
</label>
<input
type="password"
className="input input-bordered"
value={initialPassword}
onChange={(e) => setInitialPassword(e.target.value)}
placeholder="Enter password (min. 8 characters)"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Confirm Password</span>
</label>
<input
type="password"
className="input input-bordered"
value={initialConfirmPassword}
onChange={(e) => setInitialConfirmPassword(e.target.value)}
placeholder="Confirm password"
/>
</div>
{initialPasswordError && (
<div className="text-error text-sm">{initialPasswordError}</div>
)}
<p className="text-sm opacity-70">
Leave the password fields empty if you want a secure random password to be generated and sent to your personal email.
</p>
<div className="flex gap-2">
<button
className="btn btn-primary flex-1"
onClick={handleRequestEmail} onClick={handleRequestEmail}
disabled={requesting} disabled={requesting}
> >
@ -163,14 +465,19 @@ export default function EmailRequestSettings() {
Creating Email... Creating Email...
</> </>
) : ( ) : (
'Create IEEE Email Address' 'Create IEEE Email'
)} )}
</button> </button>
<button
<div className="text-xs opacity-70"> className="btn btn-outline"
<p>By requesting an email, you agree to use it responsibly and in accordance with IEEE UCSD policies.</p> onClick={toggleEmailForm}
<p>Your email address will be based on your current email username.</p> disabled={requesting}
>
Cancel
</button>
</div> </div>
</div> </div>
)}
</div>
); );
} }

View file

@ -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)

View file

@ -2,9 +2,23 @@ import type { APIRoute } from "astro";
export const POST: APIRoute = async ({ request }) => { export const POST: APIRoute = async ({ request }) => {
try { 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) { if (!userId || !name || !email) {
console.log("Missing required parameters");
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
success: false, success: false,
@ -21,12 +35,15 @@ export const POST: APIRoute = async ({ request }) => {
// Extract username from email (everything before the @ symbol) // Extract username from email (everything before the @ symbol)
const emailUsername = email.split("@")[0].toLowerCase(); const emailUsername = email.split("@")[0].toLowerCase();
console.log(`Email username extracted: ${emailUsername}`);
// Remove any special characters that might cause issues // Remove any special characters that might cause issues
const cleanUsername = emailUsername.replace(/[^a-z0-9]/g, ""); const cleanUsername = emailUsername.replace(/[^a-z0-9]/g, "");
console.log(`Cleaned username: ${cleanUsername}`);
// Generate a secure random password // Use provided password or generate a secure random one if not provided
const password = generateSecurePassword(); const newPassword = password || generateSecurePassword();
console.log(`Using ${password ? "user-provided" : "generated"} password`);
// MXRoute DirectAdmin API credentials from environment variables // MXRoute DirectAdmin API credentials from environment variables
const loginKey = import.meta.env.MXROUTE_LOGIN_KEY; 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 emailOutboundLimit = import.meta.env.MXROUTE_EMAIL_OUTBOUND_LIMIT;
const emailDomain = import.meta.env.MXROUTE_EMAIL_DOMAIN; 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) { if (!loginKey || !serverLogin || !serverUrl || !emailDomain) {
throw new Error("Missing MXRoute configuration"); throw new Error("Missing MXRoute configuration");
} }
// DirectAdmin API endpoint for creating email accounts // DirectAdmin API endpoint for creating email accounts
// According to the documentation: https://docs.directadmin.com/developer/api/legacy-api.html
let baseUrl = serverUrl; let baseUrl = serverUrl;
// If the URL contains a specific command, extract just the base URL // 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("action", "create");
formData.append("domain", emailDomain); formData.append("domain", emailDomain);
formData.append("user", cleanUsername); // DirectAdmin uses 'user' for POP accounts formData.append("user", cleanUsername); // DirectAdmin uses 'user' for POP accounts
formData.append("passwd", password); formData.append("passwd", newPassword);
formData.append("passwd2", password); formData.append("passwd2", newPassword);
formData.append("quota", emailQuota || "200"); formData.append("quota", emailQuota || "200");
formData.append("limit", emailOutboundLimit || "9600"); 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:"); console.log("Form data:");
formData.forEach((value, key) => { console.log(` action: create`);
console.log(` ${key}: ${value}`); 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, { const response = await fetch(emailApiUrl, {
method: "POST", method: "POST",
headers: { headers: {
@ -86,6 +116,7 @@ export const POST: APIRoute = async ({ request }) => {
}); });
const responseText = await response.text(); const responseText = await response.text();
console.log(`DirectAdmin response status: ${response.status}`);
console.log(`DirectAdmin response: ${responseText}`); console.log(`DirectAdmin response: ${responseText}`);
// DirectAdmin API returns "error=1" in the response text for errors // 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); throw new Error(errorMessage);
} }
// Send notification email to the user with their new email credentials console.log("Email account created successfully");
// Only send notification email if we generated a random password
if (!password) {
console.log("Sending credentials email to user");
await sendCredentialsEmail( await sendCredentialsEmail(
email, email,
`${cleanUsername}@${emailDomain}`, `${cleanUsername}@${emailDomain}`,
password, newPassword,
); );
}
// Send notification to webmaster console.log("Sending notification to webmaster");
await sendWebmasterNotification( await sendWebmasterNotification(
userId, userId,
name, name,
@ -146,8 +182,9 @@ export const POST: APIRoute = async ({ request }) => {
success: true, success: true,
data: { data: {
ieeeEmail: `${cleanUsername}@${emailDomain}`, ieeeEmail: `${cleanUsername}@${emailDomain}`,
message: message: password
"Email account created successfully. Check your email for login details.", ? "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/ - Webmail: https://heracles.mxrouting.net:2096/
- IMAP/SMTP settings can be found at: https://mxroute.com/setup/ - 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. Please change your password after your first login.
If you have any questions, please contact webmaster@ieeeucsd.org. If you have any questions, please contact webmaster@ieeeucsd.org.

View file

@ -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(/<br>/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;
}
}