added the ability to change your password

This commit is contained in:
chark1es 2025-03-08 21:53:02 -08:00
parent 01a0262ade
commit e36b5ee0ad
10 changed files with 1294 additions and 109 deletions

View file

@ -15,9 +15,31 @@ import icon from "astro-icon";
// https://astro.build/config
export default defineConfig({
output: "server",
integrations: [tailwind(), expressiveCode(), react(), icon(), mdx()],
adapter: node({
mode: "standalone",
}),
// Define environment variables that should be available to client components
vite: {
define: {
"import.meta.env.PUBLIC_LOGTO_APP_ID": JSON.stringify(
process.env.PUBLIC_LOGTO_APP_ID,
),
"import.meta.env.PUBLIC_LOGTO_APP_SECRET": JSON.stringify(
process.env.PUBLIC_LOGTO_APP_SECRET,
),
"import.meta.env.PUBLIC_LOGTO_ENDPOINT": JSON.stringify(
process.env.PUBLIC_LOGTO_ENDPOINT,
),
"import.meta.env.PUBLIC_LOGTO_TOKEN_ENDPOINT": JSON.stringify(
process.env.PUBLIC_LOGTO_TOKEN_ENDPOINT,
),
"import.meta.env.PUBLIC_LOGTO_API_ENDPOINT": JSON.stringify(
process.env.PUBLIC_LOGTO_API_ENDPOINT,
),
},
},
});

View file

@ -17,6 +17,7 @@
"astro": "5.1.1",
"astro-expressive-code": "^0.40.2",
"astro-icon": "^1.1.5",
"axios": "^1.8.2",
"chart.js": "^4.4.7",
"dexie": "^4.0.11",
"framer-motion": "^12.4.4",
@ -437,7 +438,7 @@
"autoprefixer": ["autoprefixer@10.4.20", "", { "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g=="],
"axios": ["axios@1.7.9", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw=="],
"axios": ["axios@1.8.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
@ -1427,6 +1428,8 @@
"@expressive-code/plugin-shiki/shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="],
"@iconify/tools/axios": ["axios@1.7.9", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw=="],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],

View file

@ -24,6 +24,7 @@
"astro": "5.1.1",
"astro-expressive-code": "^0.40.2",
"astro-icon": "^1.1.5",
"axios": "^1.8.2",
"chart.js": "^4.4.7",
"dexie": "^4.0.11",
"framer-motion": "^12.4.4",

View file

@ -5,120 +5,201 @@ import AccountSecuritySettings from "./SettingsSection/AccountSecuritySettings";
import NotificationSettings from "./SettingsSection/NotificationSettings";
import DisplaySettings from "./SettingsSection/DisplaySettings";
import ResumeSettings from "./SettingsSection/ResumeSettings";
// Import environment variables
const logtoAppId = import.meta.env.LOGTO_APP_ID;
const logtoAppSecret = import.meta.env.LOGTO_APP_SECRET;
const logtoEndpoint = import.meta.env.LOGTO_ENDPOINT;
const logtoTokenEndpoint = import.meta.env.LOGTO_TOKEN_ENDPOINT;
const logtoApiEndpoint = import.meta.env.LOGTO_API_ENDPOINT;
// Log environment variables for debugging
console.log("Environment variables in Astro file:");
console.log("LOGTO_APP_ID:", logtoAppId);
console.log("LOGTO_APP_SECRET:", logtoAppSecret);
console.log("LOGTO_ENDPOINT:", logtoEndpoint);
console.log("LOGTO_TOKEN_ENDPOINT:", logtoTokenEndpoint);
console.log("LOGTO_API_ENDPOINT:", logtoApiEndpoint);
// Define fallback values if environment variables are not set
const safeLogtoAppId = logtoAppId || "missing_app_id";
const safeLogtoAppSecret = logtoAppSecret || "missing_app_secret";
const safeLogtoEndpoint = logtoEndpoint || "https://auth.ieeeucsd.org";
const safeLogtoTokenEndpoint =
logtoTokenEndpoint || "https://auth.ieeeucsd.org/oidc/token";
const safeLogtoApiEndpoint = logtoApiEndpoint || "https://auth.ieeeucsd.org";
---
<div id="settings-section" class="">
<div class="mb-6">
<h2 class="text-2xl font-bold">Settings</h2>
<p class="opacity-70">Manage your account settings and preferences</p>
</div>
<div class="mb-6">
<h2 class="text-2xl font-bold">Settings</h2>
<p class="opacity-70">Manage your account settings and preferences</p>
</div>
<!-- Profile Settings Card -->
<!-- Debug Environment Variables (Only visible in development) -->
{
import.meta.env.DEV && (
<div class="card bg-base-100 shadow-xl border border-warning mb-6 p-4">
<h3 class="text-lg font-bold text-warning">
Debug Environment Variables
</h3>
<p class="text-sm mb-2">
This section is only visible in development mode
</p>
<div class="overflow-x-auto">
<table class="table table-xs">
<thead>
<tr>
<th>Variable</th>
<th>Value</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>LOGTO_APP_ID</td>
<td>{logtoAppId ? "********" : "Not set"}</td>
<td>{logtoAppId ? "✅" : "❌"}</td>
</tr>
<tr>
<td>LOGTO_APP_SECRET</td>
<td>{logtoAppSecret ? "********" : "Not set"}</td>
<td>{logtoAppSecret ? "✅" : "❌"}</td>
</tr>
<tr>
<td>LOGTO_ENDPOINT</td>
<td>{logtoEndpoint || "Not set"}</td>
<td>{logtoEndpoint ? "✅" : "❌"}</td>
</tr>
<tr>
<td>LOGTO_TOKEN_ENDPOINT</td>
<td>{logtoTokenEndpoint || "Not set"}</td>
<td>{logtoTokenEndpoint ? "✅" : "❌"}</td>
</tr>
<tr>
<td>LOGTO_API_ENDPOINT</td>
<td>{logtoApiEndpoint || "Not set"}</td>
<td>{logtoApiEndpoint ? "✅" : "❌"}</td>
</tr>
</tbody>
</table>
</div>
</div>
)
}
<!-- Profile Settings Card -->
<div
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"
>
<div class="card-body">
<h3 class="card-title flex items-center gap-3">
<div class="badge badge-primary p-3">
<Icon name="heroicons:user" class="h-5 w-5" />
</div>
Profile Information
</h3>
<p class="text-sm opacity-70 mb-4">
Update your personal information and profile details
</p>
<div class="divider"></div>
<UserProfileSettings client:load />
</div>
</div>
<!-- Resume Settings Card -->
<div
id="resume-management-section"
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"
>
<div class="card-body">
<h3 class="card-title flex items-center gap-3">
<div class="badge badge-primary p-3">
<Icon name="heroicons:document-text" class="h-5 w-5" />
</div>
Resume Management
</h3>
<p class="text-sm opacity-70 mb-4">
Upload and manage your resume for recruiters and career opportunities
</p>
<div class="divider"></div>
<ResumeSettings client:load />
</div>
</div>
<!-- Account Security Settings Card -->
<div
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"
>
<div class="card-body">
<h3 class="card-title flex items-center gap-3">
<div class="badge badge-primary p-3">
<Icon name="heroicons:lock-closed" class="h-5 w-5" />
</div>
Account Security
</h3>
<p class="text-sm opacity-70 mb-4">
Manage your account security settings and authentication options
</p>
<div class="divider"></div>
<AccountSecuritySettings
client:load
logtoAppId={safeLogtoAppId}
logtoAppSecret={safeLogtoAppSecret}
logtoEndpoint={safeLogtoEndpoint}
logtoTokenEndpoint={safeLogtoTokenEndpoint}
logtoApiEndpoint={safeLogtoApiEndpoint}
/>
</div>
</div>
<!-- Notification Settings Card -->
<div
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6 relative group"
>
<!-- Coming Soon Overlay -->
<div
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"
class="absolute inset-0 bg-base-300 bg-opacity-90 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300 z-10 rounded-xl"
>
<div class="card-body">
<h3 class="card-title flex items-center gap-3">
<div class="badge badge-primary p-3">
<Icon name="heroicons:user" class="h-5 w-5" />
</div>
Profile Information
</h3>
<p class="text-sm opacity-70 mb-4">
Update your personal information and profile details
</p>
<div class="divider"></div>
<UserProfileSettings client:load />
</div>
<div class="text-center">
<h4 class="text-xl font-bold">Coming Soon</h4>
<p class="text-sm opacity-70">
Notification settings will be available in a future update
</p>
</div>
</div>
<!-- Resume Settings Card -->
<div
id="resume-management-section"
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"
>
<div class="card-body">
<h3 class="card-title flex items-center gap-3">
<div class="badge badge-primary p-3">
<Icon name="heroicons:document-text" class="h-5 w-5" />
</div>
Resume Management
</h3>
<p class="text-sm opacity-70 mb-4">
Upload and manage your resume for recruiters and career
opportunities
</p>
<div class="divider"></div>
<ResumeSettings client:load />
<div class="card-body">
<h3 class="card-title flex items-center gap-3">
<div class="badge badge-primary p-3">
<Icon name="heroicons:bell" class="h-5 w-5" />
</div>
Notification Preferences
</h3>
<p class="text-sm opacity-70 mb-4">
Customize how and when you receive notifications
</p>
<div class="divider"></div>
<NotificationSettings client:load />
</div>
</div>
<!-- Account Security Settings Card -->
<div
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"
>
<div class="card-body">
<h3 class="card-title flex items-center gap-3">
<div class="badge badge-primary p-3">
<Icon name="heroicons:lock-closed" class="h-5 w-5" />
</div>
Account Security
</h3>
<p class="text-sm opacity-70 mb-4">
Manage your account security settings and authentication options
</p>
<div class="divider"></div>
<AccountSecuritySettings client:load />
</div>
</div>
<!-- Notification Settings Card -->
<div
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6 relative group"
>
<!-- Coming Soon Overlay -->
<div
class="absolute inset-0 bg-base-300 bg-opacity-90 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300 z-10 rounded-xl"
>
<div class="text-center">
<h4 class="text-xl font-bold">Coming Soon</h4>
<p class="text-sm opacity-70">
Notification settings will be available in a future update
</p>
</div>
</div>
<div class="card-body">
<h3 class="card-title flex items-center gap-3">
<div class="badge badge-primary p-3">
<Icon name="heroicons:bell" class="h-5 w-5" />
</div>
Notification Preferences
</h3>
<p class="text-sm opacity-70 mb-4">
Customize how and when you receive notifications
</p>
<div class="divider"></div>
<NotificationSettings client:load />
</div>
</div>
<!-- Display Settings Card -->
<div
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform"
>
<div class="card-body">
<h3 class="card-title flex items-center gap-3">
<div class="badge badge-primary p-3">
<Icon name="heroicons:computer-desktop" class="h-5 w-5" />
</div>
Display Settings
</h3>
<p class="text-sm opacity-70 mb-4">
Customize your dashboard appearance and display preferences
</p>
<div class="divider"></div>
<DisplaySettings client:load />
<!-- Display Settings Card -->
<div
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform"
>
<div class="card-body">
<h3 class="card-title flex items-center gap-3">
<div class="badge badge-primary p-3">
<Icon name="heroicons:computer-desktop" class="h-5 w-5" />
</div>
Display Settings
</h3>
<p class="text-sm opacity-70 mb-4">
Customize your dashboard appearance and display preferences
</p>
<div class="divider"></div>
<DisplaySettings client:load />
</div>
</div>
</div>

View file

@ -2,8 +2,23 @@ import { useState, useEffect } from 'react';
import { Authentication } from '../../../scripts/pocketbase/Authentication';
import { SendLog } from '../../../scripts/pocketbase/SendLog';
import { toast } from 'react-hot-toast';
import PasswordChangeSettings from './PasswordChangeSettings';
export default function AccountSecuritySettings() {
interface AccountSecuritySettingsProps {
logtoAppId: string;
logtoAppSecret: string;
logtoEndpoint: string;
logtoTokenEndpoint: string;
logtoApiEndpoint: string;
}
export default function AccountSecuritySettings({
logtoAppId,
logtoAppSecret,
logtoEndpoint,
logtoTokenEndpoint,
logtoApiEndpoint
}: AccountSecuritySettingsProps) {
const auth = Authentication.getInstance();
const logger = SendLog.getInstance();
const [isAuthenticated, setIsAuthenticated] = useState(false);
@ -126,6 +141,21 @@ export default function AccountSecuritySettings() {
</div>
</div>
{/* Password Change Section */}
<div>
<h4 className="font-semibold text-lg mb-2">Change Password</h4>
<p className="text-sm opacity-70 mb-4">
Update your account password. For security reasons, you'll need to provide your current password.
</p>
<PasswordChangeSettings
logtoAppId={logtoAppId}
logtoAppSecret={logtoAppSecret}
logtoEndpoint={logtoEndpoint}
logtoTokenEndpoint={logtoTokenEndpoint}
logtoApiEndpoint={logtoApiEndpoint}
/>
</div>
{/* Authentication Options */}
<div>
<h4 className="font-semibold text-lg mb-2">Authentication Options</h4>
@ -133,10 +163,6 @@ export default function AccountSecuritySettings() {
IEEE UCSD uses Single Sign-On (SSO) for authentication.
Password management is handled through your IEEEUCSD account.
</p>
<p className="text-sm text-info p-3 bg-info bg-opacity-10 rounded-lg">
To change your password, please use the "Forgot Password" option on the login page.
</p>
</div>
{/* Account Actions */}

View file

@ -0,0 +1,567 @@
import { useState, useEffect } from 'react';
import { toast } from 'react-hot-toast';
import { Authentication } from '../../../scripts/pocketbase/Authentication';
import { SendLog } from '../../../scripts/pocketbase/SendLog';
interface PasswordChangeSettingsProps {
logtoAppId?: string;
logtoAppSecret?: string;
logtoEndpoint?: string;
logtoTokenEndpoint?: string;
logtoApiEndpoint?: string;
}
export default function PasswordChangeSettings({
logtoAppId: propLogtoAppId,
logtoAppSecret: propLogtoAppSecret,
logtoEndpoint: propLogtoEndpoint,
logtoTokenEndpoint: propLogtoTokenEndpoint,
logtoApiEndpoint: propLogtoApiEndpoint
}: PasswordChangeSettingsProps) {
const auth = Authentication.getInstance();
const logger = SendLog.getInstance();
const [isLoading, setIsLoading] = useState(false);
const [isCheckingEnv, setIsCheckingEnv] = useState(false);
const [useFormSubmission, setUseFormSubmission] = useState(false); // Default to using JSON
const [formData, setFormData] = useState({
currentPassword: '',
newPassword: '',
confirmPassword: '',
});
const [logtoUserId, setLogtoUserId] = useState('');
const [debugInfo, setDebugInfo] = useState<any>(null);
// Access environment variables directly
const envLogtoAppId = import.meta.env.PUBLIC_LOGTO_APP_ID;
const envLogtoAppSecret = import.meta.env.PUBLIC_LOGTO_APP_SECRET;
const envLogtoEndpoint = import.meta.env.PUBLIC_LOGTO_ENDPOINT;
const envLogtoTokenEndpoint = import.meta.env.PUBLIC_LOGTO_TOKEN_ENDPOINT;
const envLogtoApiEndpoint = import.meta.env.PUBLIC_LOGTO_API_ENDPOINT;
// Use environment variables or props (fallback)
const logtoAppId = envLogtoAppId || propLogtoAppId;
const logtoAppSecret = envLogtoAppSecret || propLogtoAppSecret;
const logtoEndpoint = envLogtoEndpoint || propLogtoEndpoint;
const logtoTokenEndpoint = envLogtoTokenEndpoint || propLogtoTokenEndpoint;
const logtoApiEndpoint = envLogtoApiEndpoint || propLogtoApiEndpoint;
// Log props and environment variables for debugging
useEffect(() => {
console.log("PasswordChangeSettings props and env vars:");
console.log("Props - logtoAppId:", propLogtoAppId);
console.log("Props - logtoAppSecret:", propLogtoAppSecret);
console.log("Props - logtoEndpoint:", propLogtoEndpoint);
console.log("Props - logtoTokenEndpoint:", propLogtoTokenEndpoint);
console.log("Props - logtoApiEndpoint:", propLogtoApiEndpoint);
console.log("Env - PUBLIC_LOGTO_APP_ID:", envLogtoAppId);
console.log("Env - PUBLIC_LOGTO_APP_SECRET:", envLogtoAppSecret);
console.log("Env - PUBLIC_LOGTO_ENDPOINT:", envLogtoEndpoint);
console.log("Env - PUBLIC_LOGTO_TOKEN_ENDPOINT:", envLogtoTokenEndpoint);
console.log("Env - PUBLIC_LOGTO_API_ENDPOINT:", envLogtoApiEndpoint);
console.log("Using - logtoAppId:", logtoAppId);
console.log("Using - logtoAppSecret:", logtoAppSecret);
console.log("Using - logtoEndpoint:", logtoEndpoint);
console.log("Using - logtoTokenEndpoint:", logtoTokenEndpoint);
console.log("Using - logtoApiEndpoint:", logtoApiEndpoint);
}, [
propLogtoAppId, propLogtoAppSecret, propLogtoEndpoint, propLogtoTokenEndpoint, propLogtoApiEndpoint,
envLogtoAppId, envLogtoAppSecret, envLogtoEndpoint, envLogtoTokenEndpoint, envLogtoApiEndpoint,
logtoAppId, logtoAppSecret, logtoEndpoint, logtoTokenEndpoint, logtoApiEndpoint
]);
// Get the user's Logto ID on component mount
useEffect(() => {
const fetchLogtoUserId = async () => {
try {
const user = auth.getCurrentUser();
if (!user) {
console.error("User not authenticated");
toast.error("You must be logged in to change your password");
return;
}
console.log("Current user:", user);
const pb = auth.getPocketBase();
try {
const externalAuthRecord = await pb.collection('_externalAuths').getFirstListItem(`recordRef="${user.id}" && provider="oidc"`);
console.log("Found external auth record:", externalAuthRecord);
const userId = externalAuthRecord.providerId;
if (userId) {
setLogtoUserId(userId);
console.log("Set Logto user ID:", userId);
} else {
console.error("No providerId found in external auth record");
toast.error("Could not determine your user ID. Please try again later or contact support.");
}
} catch (error) {
console.error("Error fetching external auth record:", error);
toast.error("Error retrieving your user information. Please try again later.");
// Try to get more information about the error
if (error instanceof Error) {
console.error("Error details:", error.message);
if ('data' in error) {
console.error("Error data:", (error as any).data);
}
}
}
} catch (error) {
console.error("Error fetching Logto user ID:", error);
toast.error("Error retrieving your user information. Please try again later.");
}
};
fetchLogtoUserId();
}, []);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
};
const validateForm = () => {
if (!formData.currentPassword) {
toast.error('Current password is required');
return false;
}
if (!formData.newPassword) {
toast.error('New password is required');
return false;
}
if (formData.newPassword.length < 8) {
toast.error('New password must be at least 8 characters long');
return false;
}
if (formData.newPassword !== formData.confirmPassword) {
toast.error('New passwords do not match');
return false;
}
if (!logtoUserId) {
toast.error('Could not determine your user ID. Please try again later.');
return false;
}
return true;
};
const checkEnvironmentVariables = async () => {
setIsCheckingEnv(true);
try {
const response = await fetch('/api/check-env');
const data = await response.json();
console.log("Environment variables status:", data);
// Check if all required environment variables are set
const { envStatus } = data;
const missingVars = Object.entries(envStatus)
.filter(([_, isSet]) => !isSet)
.map(([name]) => name);
if (missingVars.length > 0) {
toast.error(`Missing environment variables: ${missingVars.join(', ')}`);
} else {
toast.success('All environment variables are set');
}
} catch (error) {
console.error("Error checking environment variables:", error);
toast.error('Failed to check environment variables');
} finally {
setIsCheckingEnv(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) return;
setIsLoading(true);
setDebugInfo(null);
try {
if (useFormSubmission) {
// Use a traditional form submission approach
const formElement = document.createElement('form');
formElement.method = 'POST';
formElement.action = '/api/change-password';
formElement.enctype = 'application/x-www-form-urlencoded';
// Add the userId field
const userIdField = document.createElement('input');
userIdField.type = 'hidden';
userIdField.name = 'userId';
userIdField.value = logtoUserId;
formElement.appendChild(userIdField);
// Add the newPassword field
const newPasswordField = document.createElement('input');
newPasswordField.type = 'hidden';
newPasswordField.name = 'newPassword';
newPasswordField.value = formData.newPassword;
formElement.appendChild(newPasswordField);
// If not using hardcoded endpoint, add the Logto credentials
const logtoAppIdField = document.createElement('input');
logtoAppIdField.type = 'hidden';
logtoAppIdField.name = 'logtoAppId';
logtoAppIdField.value = logtoAppId;
formElement.appendChild(logtoAppIdField);
const logtoAppSecretField = document.createElement('input');
logtoAppSecretField.type = 'hidden';
logtoAppSecretField.name = 'logtoAppSecret';
logtoAppSecretField.value = logtoAppSecret;
formElement.appendChild(logtoAppSecretField);
const logtoTokenEndpointField = document.createElement('input');
logtoTokenEndpointField.type = 'hidden';
logtoTokenEndpointField.name = 'logtoTokenEndpoint';
logtoTokenEndpointField.value = logtoTokenEndpoint;
formElement.appendChild(logtoTokenEndpointField);
const logtoApiEndpointField = document.createElement('input');
logtoApiEndpointField.type = 'hidden';
logtoApiEndpointField.name = 'logtoApiEndpoint';
logtoApiEndpointField.value = logtoApiEndpoint;
formElement.appendChild(logtoApiEndpointField);
// Create an iframe to handle the form submission
const iframe = document.createElement('iframe');
iframe.name = 'password-change-frame';
iframe.style.display = 'none';
document.body.appendChild(iframe);
// Set up the iframe load event to handle the response
iframe.onload = async () => {
try {
// Try to get the response from the iframe
const iframeDocument = iframe.contentDocument || iframe.contentWindow?.document;
if (iframeDocument) {
const responseText = iframeDocument.body.innerText;
console.log("Response from iframe:", responseText);
if (responseText) {
try {
const result = JSON.parse(responseText);
if (result.success) {
// Log the password change
await logger.send('update', 'password', 'User changed their password');
toast.success('Password changed successfully');
// Clear form
setFormData({
currentPassword: '',
newPassword: '',
confirmPassword: '',
});
} else {
toast.error(result.message || 'Failed to change password');
}
} catch (error) {
console.error("Error parsing response:", error);
toast.error('Failed to parse response from server');
}
} else {
// If no response text, assume success
// Log the password change
await logger.send('update', 'password', 'User changed their password');
toast.success('Password changed successfully');
// Clear form
setFormData({
currentPassword: '',
newPassword: '',
confirmPassword: '',
});
}
} else {
console.error("Could not access iframe document");
toast.error('Could not access response from server');
}
} catch (error) {
console.error("Error handling iframe response:", error);
toast.error('Error handling response from server');
} finally {
// Clean up
document.body.removeChild(iframe);
setIsLoading(false);
}
};
// Set the target to the iframe
formElement.target = 'password-change-frame';
// Append the form to the document, submit it, and then remove it
document.body.appendChild(formElement);
formElement.submit();
document.body.removeChild(formElement);
// Note: setIsLoading(false) is called in the iframe.onload handler
} else {
// Use the fetch API with JSON
const endpoint = '/api/change-password';
console.log(`Calling server-side API endpoint: ${endpoint}`);
// Ensure we have the Logto user ID
if (!logtoUserId) {
console.error("Logto user ID is missing");
throw new Error("User ID is missing. Please try again or contact support.");
}
// Log the values we're about to use
console.log("Values being used for API call:");
console.log("- logtoUserId:", logtoUserId);
console.log("- newPassword:", formData.newPassword ? "[PRESENT]" : "[MISSING]");
console.log("- logtoAppId:", logtoAppId);
console.log("- logtoAppSecret:", logtoAppSecret ? "[PRESENT]" : "[MISSING]");
console.log("- logtoTokenEndpoint:", logtoTokenEndpoint);
console.log("- logtoApiEndpoint:", logtoApiEndpoint);
// Prepare request data with explicit values (not relying on variable references that might be undefined)
const requestData = {
userId: logtoUserId,
newPassword: formData.newPassword,
logtoAppId: logtoAppId || "",
logtoAppSecret: logtoAppSecret || "",
logtoTokenEndpoint: logtoTokenEndpoint || `${logtoEndpoint}/oidc/token`,
logtoApiEndpoint: logtoApiEndpoint || logtoEndpoint
};
console.log("Request data:", {
...requestData,
newPassword: "[REDACTED]",
logtoAppSecret: "[REDACTED]"
});
// Validate request data before sending
if (!requestData.userId) {
throw new Error("Missing userId. Please try again or contact support.");
}
if (!requestData.newPassword) {
throw new Error("Missing newPassword. Please enter a new password.");
}
if (!requestData.logtoAppId) {
throw new Error("Missing logtoAppId configuration. Please contact support.");
}
if (!requestData.logtoAppSecret) {
throw new Error("Missing logtoAppSecret configuration. Please contact support.");
}
if (!requestData.logtoTokenEndpoint) {
throw new Error("Missing logtoTokenEndpoint configuration. Please contact support.");
}
if (!requestData.logtoApiEndpoint) {
throw new Error("Missing logtoApiEndpoint configuration. Please contact support.");
}
// Stringify the request data to ensure it's valid JSON
const requestBody = JSON.stringify(requestData);
console.log("Request body (stringified):", requestBody);
// Create a debug object to display in the UI
const debugObj = {
endpoint,
requestData,
requestBody,
logtoUserId,
hasNewPassword: !!formData.newPassword,
hasLogtoAppId: !!logtoAppId,
hasLogtoAppSecret: !!logtoAppSecret,
hasLogtoTokenEndpoint: !!logtoTokenEndpoint,
hasLogtoApiEndpoint: !!logtoApiEndpoint
};
setDebugInfo(debugObj);
// Call our server-side API endpoint to change the password
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: requestBody
});
console.log("Response status:", response.status);
// Process the response
let result: any;
try {
const responseText = await response.text();
console.log("Raw response:", responseText);
if (responseText) {
result = JSON.parse(responseText);
} else {
result = { success: false, message: 'Empty response from server' };
}
console.log("API response:", result);
// Add response to debug info
setDebugInfo((prev: any) => ({
...prev,
responseStatus: response.status,
responseText,
parsedResponse: result
}));
} catch (error) {
console.error("Error parsing API response:", error);
setDebugInfo((prev: any) => ({
...prev,
responseError: error instanceof Error ? error.message : String(error)
}));
throw new Error(`Invalid response from server: ${error instanceof Error ? error.message : String(error)}`);
}
// Check if the request was successful
if (response.ok && result.success) {
// Log the password change
await logger.send('update', 'password', 'User changed their password');
toast.success('Password changed successfully');
// Clear form
setFormData({
currentPassword: '',
newPassword: '',
confirmPassword: '',
});
} else {
throw new Error(result.message || `Failed to change password: ${response.status}`);
}
setIsLoading(false);
}
} catch (error) {
console.error('Error changing password:', error);
toast.error(error instanceof Error ? error.message : 'Failed to change password');
setIsLoading(false);
}
};
return (
<div>
{process.env.NODE_ENV === 'development' && (
<div className="mb-4 p-3 bg-base-200 rounded-lg">
<h4 className="text-sm font-semibold mb-2">Debug Tools (Development Only)</h4>
<div className="flex flex-wrap gap-2 mb-2">
<button
type="button"
className={`btn btn-sm btn-info ${isCheckingEnv ? 'loading' : ''}`}
onClick={checkEnvironmentVariables}
disabled={isCheckingEnv}
>
Check Environment Variables
</button>
<button
type="button"
className="btn btn-sm btn-warning"
onClick={() => {
console.log("Debug Info:");
console.log("- logtoUserId:", logtoUserId);
console.log("- Environment Variables:");
console.log(" - PUBLIC_LOGTO_APP_ID:", import.meta.env.PUBLIC_LOGTO_APP_ID);
console.log(" - PUBLIC_LOGTO_ENDPOINT:", import.meta.env.PUBLIC_LOGTO_ENDPOINT);
console.log(" - PUBLIC_LOGTO_TOKEN_ENDPOINT:", import.meta.env.PUBLIC_LOGTO_TOKEN_ENDPOINT);
console.log(" - PUBLIC_LOGTO_API_ENDPOINT:", import.meta.env.PUBLIC_LOGTO_API_ENDPOINT);
toast.success("Debug info logged to console");
}}
>
Log Debug Info
</button>
<button
type="button"
className={`btn btn-sm ${!useFormSubmission ? 'btn-success' : 'btn-outline'}`}
onClick={() => setUseFormSubmission(false)}
>
Use JSON API
</button>
</div>
<div className="mt-2 text-xs">
<p>Using fixed LogTo implementation with {useFormSubmission ? 'form submission' : 'JSON'}</p>
<p>Logto User ID: {logtoUserId || 'Not found'}</p>
</div>
{debugInfo && (
<div className="mt-4 border-t pt-2">
<p className="font-semibold">Debug Info:</p>
<div className="overflow-auto max-h-60 bg-base-300 p-2 rounded text-xs">
<pre>{JSON.stringify(debugInfo, null, 2)}</pre>
</div>
</div>
)}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="form-control">
<label className="label">
<span className="label-text">Current Password</span>
</label>
<input
type="password"
name="currentPassword"
value={formData.currentPassword}
onChange={handleInputChange}
className="input input-bordered w-full"
required
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">New Password</span>
</label>
<input
type="password"
name="newPassword"
value={formData.newPassword}
onChange={handleInputChange}
className="input input-bordered w-full"
required
minLength={8}
/>
<label className="label">
<span className="label-text-alt">Password must be at least 8 characters long</span>
</label>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Confirm New Password</span>
</label>
<input
type="password"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleInputChange}
className="input input-bordered w-full"
required
/>
</div>
<div className="form-control mt-6">
<button
type="submit"
className={`btn btn-primary ${isLoading ? 'loading' : ''}`}
disabled={isLoading}
>
{isLoading ? 'Changing Password...' : 'Change Password'}
</button>
</div>
</form>
</div>
);
}

103
src/env.ts Normal file
View file

@ -0,0 +1,103 @@
import { z } from "zod";
/**
* Define a schema for environment variables with Zod
* This provides type safety and validation for environment variables
*/
const envSchema = z.object({
// LogTo Configuration
LOGTO_APP_ID: z.string().min(1, "LOGTO_APP_ID is required"),
LOGTO_APP_SECRET: z.string().min(1, "LOGTO_APP_SECRET is required"),
LOGTO_ENDPOINT: z.string().url("LOGTO_ENDPOINT must be a valid URL"),
LOGTO_TOKEN_ENDPOINT: z
.string()
.url("LOGTO_TOKEN_ENDPOINT must be a valid URL"),
LOGTO_API_ENDPOINT: z.string().url("LOGTO_API_ENDPOINT must be a valid URL"),
LOGTO_USERINFO_ENDPOINT: z
.string()
.url("LOGTO_USERINFO_ENDPOINT must be a valid URL"),
// API Base URL (optional)
API_BASE_URL: z.string().url("API_BASE_URL must be a valid URL").optional(),
});
/**
* Parse and validate environment variables
* This will throw an error if any required variables are missing or invalid
*/
function getEnvVariables() {
// In development, use import.meta.env (Vite/Astro)
if (import.meta.env) {
const envVars = {
LOGTO_APP_ID: import.meta.env.LOGTO_APP_ID,
LOGTO_APP_SECRET: import.meta.env.LOGTO_APP_SECRET,
LOGTO_ENDPOINT: import.meta.env.LOGTO_ENDPOINT,
LOGTO_TOKEN_ENDPOINT: import.meta.env.LOGTO_TOKEN_ENDPOINT,
LOGTO_API_ENDPOINT: import.meta.env.LOGTO_API_ENDPOINT,
LOGTO_USERINFO_ENDPOINT: import.meta.env.LOGTO_USERINFO_ENDPOINT,
API_BASE_URL: import.meta.env.API_BASE_URL,
};
try {
return envSchema.parse(envVars);
} catch (error) {
console.error("Environment variable validation failed:", error);
// Log which variables are missing or invalid
if (error instanceof z.ZodError) {
error.errors.forEach((err) => {
console.error(`- ${err.path.join(".")}: ${err.message}`);
});
}
// Return default values for development with warnings
console.warn(
"Using fallback values for development. DO NOT USE IN PRODUCTION!",
);
return {
LOGTO_APP_ID: import.meta.env.LOGTO_APP_ID || "development_app_id",
LOGTO_APP_SECRET:
import.meta.env.LOGTO_APP_SECRET || "development_app_secret",
LOGTO_ENDPOINT:
import.meta.env.LOGTO_ENDPOINT || "https://auth.ieeeucsd.org",
LOGTO_TOKEN_ENDPOINT:
import.meta.env.LOGTO_TOKEN_ENDPOINT ||
"https://auth.ieeeucsd.org/oidc/token",
LOGTO_API_ENDPOINT:
import.meta.env.LOGTO_API_ENDPOINT || "https://auth.ieeeucsd.org",
LOGTO_USERINFO_ENDPOINT:
import.meta.env.LOGTO_USERINFO_ENDPOINT ||
"https://auth.ieeeucsd.org/oidc/me",
API_BASE_URL: import.meta.env.API_BASE_URL || "http://localhost:4321",
};
}
}
// In Node.js environment (server-side)
if (typeof process !== "undefined" && process.env) {
const envVars = {
LOGTO_APP_ID: process.env.LOGTO_APP_ID || "",
LOGTO_APP_SECRET: process.env.LOGTO_APP_SECRET || "",
LOGTO_ENDPOINT: process.env.LOGTO_ENDPOINT || "",
LOGTO_TOKEN_ENDPOINT: process.env.LOGTO_TOKEN_ENDPOINT || "",
LOGTO_API_ENDPOINT: process.env.LOGTO_API_ENDPOINT || "",
LOGTO_USERINFO_ENDPOINT: process.env.LOGTO_USERINFO_ENDPOINT || "",
API_BASE_URL: process.env.API_BASE_URL,
};
try {
return envSchema.parse(envVars);
} catch (error) {
console.error("Environment variable validation failed:", error);
throw new Error("Missing or invalid environment variables");
}
}
// Fallback for other environments
throw new Error("Unable to load environment variables");
}
// Export the validated environment variables
export const env = getEnvVariables();
// Type definition for the environment variables
export type Env = z.infer<typeof envSchema>;

View file

@ -0,0 +1,149 @@
# 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

@ -0,0 +1,189 @@
import type { APIRoute } from "astro";
// Mark this endpoint as server-rendered, not static
export const prerender = false;
// Helper function to get Logto access token
async function getLogtoAccessToken(
logtoTokenEndpoint: string,
clientId: string,
clientSecret: string,
scope: string = "all",
): Promise<string | null> {
try {
console.log("Attempting to get access token from Logto");
// Create Basic auth string
const authString = Buffer.from(`${clientId}:${clientSecret}`).toString(
"base64",
);
const params = new URLSearchParams({
grant_type: "client_credentials",
resource: "https://default.logto.app/api",
scope: scope,
});
const response = await fetch(logtoTokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${authString}`,
},
body: params,
});
if (!response.ok) {
const errorText = await response.text();
console.error("Token request error:", {
status: response.status,
statusText: response.statusText,
body: errorText,
});
return null;
}
const data = await response.json();
return data.access_token || null;
} catch (error) {
console.error("Network error fetching token:", error);
return null;
}
}
export const POST: APIRoute = async ({ request }) => {
try {
console.log("Received change password request");
// Parse request body
const contentType = request.headers.get("content-type");
const rawBody = await request.text();
let data;
if (contentType?.includes("application/json")) {
data = JSON.parse(rawBody);
} else if (contentType?.includes("application/x-www-form-urlencoded")) {
const formData = new URLSearchParams(rawBody);
data = Object.fromEntries(formData.entries());
} else {
return new Response(
JSON.stringify({
success: false,
message: "Unsupported content type. Please use JSON or form data.",
}),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}
// Extract required parameters
const {
userId,
newPassword,
logtoAppId,
logtoAppSecret,
logtoTokenEndpoint,
logtoApiEndpoint,
} = data;
// Validate required parameters
const missingParams = [];
if (!userId) missingParams.push("userId");
if (!newPassword) missingParams.push("newPassword");
if (!logtoAppId) missingParams.push("logtoAppId");
if (!logtoAppSecret) missingParams.push("logtoAppSecret");
if (!logtoTokenEndpoint) missingParams.push("logtoTokenEndpoint");
if (!logtoApiEndpoint) missingParams.push("logtoApiEndpoint");
if (missingParams.length > 0) {
return new Response(
JSON.stringify({
success: false,
message: `Missing required parameters: ${missingParams.join(", ")}`,
}),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}
// Get access token
const accessToken = await getLogtoAccessToken(
logtoTokenEndpoint,
logtoAppId,
logtoAppSecret,
);
if (!accessToken) {
return new Response(
JSON.stringify({
success: false,
message: "Failed to obtain access token from Logto",
}),
{
status: 500,
headers: { "Content-Type": "application/json" },
},
);
}
// Change password using Logto Management API
const changePasswordResponse = await fetch(
`${logtoApiEndpoint}/api/users/${userId}/password`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ password: newPassword }),
},
);
if (!changePasswordResponse.ok) {
const errorData = await changePasswordResponse.json().catch(() => ({
message: "Could not read error response",
}));
return new Response(
JSON.stringify({
success: false,
message:
errorData.message ||
`Failed to change password: ${changePasswordResponse.status} ${changePasswordResponse.statusText}`,
}),
{
status: changePasswordResponse.status,
headers: { "Content-Type": "application/json" },
},
);
}
return new Response(
JSON.stringify({
success: true,
message: "Password changed successfully",
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
},
);
} catch (error) {
console.error("Unexpected error in change-password API:", error);
return new Response(
JSON.stringify({
success: false,
message:
error instanceof Error ? error.message : "An unknown error occurred",
}),
{
status: 500,
headers: { "Content-Type": "application/json" },
},
);
}
};

View file

@ -0,0 +1,44 @@
import type { APIRoute } from "astro";
// Mark this endpoint as server-rendered, not static
export const prerender = false;
export const GET: APIRoute = async () => {
// Get environment variables
const logtoAppId = import.meta.env.LOGTO_APP_ID;
const logtoAppSecret = import.meta.env.LOGTO_APP_SECRET;
const logtoEndpoint = import.meta.env.LOGTO_ENDPOINT;
const logtoTokenEndpoint = import.meta.env.LOGTO_TOKEN_ENDPOINT;
const logtoApiEndpoint = import.meta.env.LOGTO_API_ENDPOINT;
// Check which environment variables are set
const envStatus = {
LOGTO_APP_ID: !!logtoAppId,
LOGTO_APP_SECRET: !!logtoAppSecret,
LOGTO_ENDPOINT: !!logtoEndpoint,
LOGTO_TOKEN_ENDPOINT: !!logtoTokenEndpoint,
LOGTO_API_ENDPOINT: !!logtoApiEndpoint,
};
// Return the status
return new Response(
JSON.stringify({
message: "Environment variables status",
envStatus,
// Include the actual values for debugging (except secrets)
envValues: {
LOGTO_APP_ID: logtoAppId ? "********" : null,
LOGTO_APP_SECRET: logtoAppSecret ? "********" : null,
LOGTO_ENDPOINT: logtoEndpoint,
LOGTO_TOKEN_ENDPOINT: logtoTokenEndpoint,
LOGTO_API_ENDPOINT: logtoApiEndpoint,
},
}),
{
status: 200,
headers: {
"Content-Type": "application/json",
},
},
);
};