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
|
||||
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,
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
5
bun.lock
5
bun.lock
|
@ -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=="],
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 */}
|
||||
|
|
|
@ -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