added the ability to change your password
This commit is contained in:
parent
01a0262ade
commit
e36b5ee0ad
10 changed files with 1294 additions and 109 deletions
|
@ -15,9 +15,31 @@ import icon from "astro-icon";
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
output: "server",
|
||||||
integrations: [tailwind(), expressiveCode(), react(), icon(), mdx()],
|
integrations: [tailwind(), expressiveCode(), react(), icon(), mdx()],
|
||||||
|
|
||||||
adapter: node({
|
adapter: node({
|
||||||
mode: "standalone",
|
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,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
5
bun.lock
5
bun.lock
|
@ -17,6 +17,7 @@
|
||||||
"astro": "5.1.1",
|
"astro": "5.1.1",
|
||||||
"astro-expressive-code": "^0.40.2",
|
"astro-expressive-code": "^0.40.2",
|
||||||
"astro-icon": "^1.1.5",
|
"astro-icon": "^1.1.5",
|
||||||
|
"axios": "^1.8.2",
|
||||||
"chart.js": "^4.4.7",
|
"chart.js": "^4.4.7",
|
||||||
"dexie": "^4.0.11",
|
"dexie": "^4.0.11",
|
||||||
"framer-motion": "^12.4.4",
|
"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=="],
|
"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=="],
|
"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=="],
|
"@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/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=="],
|
"@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=="],
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
"astro": "5.1.1",
|
"astro": "5.1.1",
|
||||||
"astro-expressive-code": "^0.40.2",
|
"astro-expressive-code": "^0.40.2",
|
||||||
"astro-icon": "^1.1.5",
|
"astro-icon": "^1.1.5",
|
||||||
|
"axios": "^1.8.2",
|
||||||
"chart.js": "^4.4.7",
|
"chart.js": "^4.4.7",
|
||||||
"dexie": "^4.0.11",
|
"dexie": "^4.0.11",
|
||||||
"framer-motion": "^12.4.4",
|
"framer-motion": "^12.4.4",
|
||||||
|
|
|
@ -5,6 +5,29 @@ import AccountSecuritySettings from "./SettingsSection/AccountSecuritySettings";
|
||||||
import NotificationSettings from "./SettingsSection/NotificationSettings";
|
import NotificationSettings from "./SettingsSection/NotificationSettings";
|
||||||
import DisplaySettings from "./SettingsSection/DisplaySettings";
|
import DisplaySettings from "./SettingsSection/DisplaySettings";
|
||||||
import ResumeSettings from "./SettingsSection/ResumeSettings";
|
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 id="settings-section" class="">
|
||||||
|
@ -13,6 +36,58 @@ import ResumeSettings from "./SettingsSection/ResumeSettings";
|
||||||
<p class="opacity-70">Manage your account settings and preferences</p>
|
<p class="opacity-70">Manage your account settings and preferences</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 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 -->
|
<!-- Profile Settings Card -->
|
||||||
<div
|
<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="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"
|
||||||
|
@ -45,8 +120,7 @@ import ResumeSettings from "./SettingsSection/ResumeSettings";
|
||||||
Resume Management
|
Resume Management
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-sm opacity-70 mb-4">
|
<p class="text-sm opacity-70 mb-4">
|
||||||
Upload and manage your resume for recruiters and career
|
Upload and manage your resume for recruiters and career opportunities
|
||||||
opportunities
|
|
||||||
</p>
|
</p>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<ResumeSettings client:load />
|
<ResumeSettings client:load />
|
||||||
|
@ -68,7 +142,14 @@ import ResumeSettings from "./SettingsSection/ResumeSettings";
|
||||||
Manage your account security settings and authentication options
|
Manage your account security settings and authentication options
|
||||||
</p>
|
</p>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<AccountSecuritySettings client:load />
|
<AccountSecuritySettings
|
||||||
|
client:load
|
||||||
|
logtoAppId={safeLogtoAppId}
|
||||||
|
logtoAppSecret={safeLogtoAppSecret}
|
||||||
|
logtoEndpoint={safeLogtoEndpoint}
|
||||||
|
logtoTokenEndpoint={safeLogtoTokenEndpoint}
|
||||||
|
logtoApiEndpoint={safeLogtoApiEndpoint}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,23 @@ import { useState, useEffect } from 'react';
|
||||||
import { Authentication } from '../../../scripts/pocketbase/Authentication';
|
import { Authentication } from '../../../scripts/pocketbase/Authentication';
|
||||||
import { SendLog } from '../../../scripts/pocketbase/SendLog';
|
import { SendLog } from '../../../scripts/pocketbase/SendLog';
|
||||||
import { toast } from 'react-hot-toast';
|
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 auth = Authentication.getInstance();
|
||||||
const logger = SendLog.getInstance();
|
const logger = SendLog.getInstance();
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
|
@ -126,6 +141,21 @@ export default function AccountSecuritySettings() {
|
||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Authentication Options */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-semibold text-lg mb-2">Authentication Options</h4>
|
<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.
|
IEEE UCSD uses Single Sign-On (SSO) for authentication.
|
||||||
Password management is handled through your IEEEUCSD account.
|
Password management is handled through your IEEEUCSD account.
|
||||||
</p>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Account Actions */}
|
{/* Account Actions */}
|
||||||
|
|
|
@ -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
103
src/env.ts
Normal 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>;
|
149
src/pages/api/README-password-change.md
Normal file
149
src/pages/api/README-password-change.md
Normal 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)
|
189
src/pages/api/change-password.ts
Normal file
189
src/pages/api/change-password.ts
Normal 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" },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
44
src/pages/api/check-env.ts
Normal file
44
src/pages/api/check-env.ts
Normal 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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in a new issue