sp/src/components/PasswordUpdateCard.tsx

287 lines
9.6 KiB
TypeScript

import { useState, useEffect } from 'react';
import PocketBase from 'pocketbase';
import { Input } from "./ui/input";
import { Button } from "./ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
import { Eye, EyeOff, Loader2 } from "lucide-react";
import { Label } from "./ui/label";
// import type { RecordModel } from 'pocketbase';
const PUBLIC_POCKETBASE_URL = import.meta.env.PUBLIC_POCKETBASE_URL;
const pb = new PocketBase(PUBLIC_POCKETBASE_URL);
interface RecordModel {
id: string;
collectionId: string;
collectionName: string;
created: string;
updated: string;
[key: string]: any;
}
interface UserRecord extends RecordModel {
email: string;
verified?: boolean;
emailVisibility?: boolean;
username?: string;
name?: string;
avatar?: string;
provider?: string;
providerData?: {
[key: string]: {
id: string;
name?: string;
email?: string;
avatarUrl?: string;
}
};
lastPasswordReset?: string;
passwordHash?: string;
}
const PasswordUpdateCard = ({ userId, onLogout }: { userId: string, onLogout: () => void }) => {
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
const [showNewPassword, setShowNewPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [status, setStatus] = useState({ message: '', isError: false });
const [isLoading, setIsLoading] = useState(false);
const [isOAuthUser, setIsOAuthUser] = useState(false);
useEffect(() => {
const checkOAuthStatus = async () => {
try {
// Get user with all fields (including internal ones)
const user = await pb.collection('users').getOne(userId, {
fields: '*,provider,verified,passwordHash'
});
// New reliable OAuth detection logic:
const isOAuthUser = (
// Case 1: No password but verified (typical OAuth pattern)
(!user.passwordHash && user.verified) ||
// Case 2: Has provider specified (older OAuth users)
(user.provider && user.provider !== 'email') ||
// Case 3: Check externalId field if exists (some OAuth implementations)
(user.externalId && typeof user.externalId === 'string')
);
console.log('OAuth determination:', {
email: user.email,
hasPassword: !!user.passwordHash,
verified: user.verified,
provider: user.provider,
isOAuthUser
});
setIsOAuthUser(isOAuthUser);
} catch (error) {
console.error("Failed to fetch user data:", error);
// Fallback to checking verification status
setIsOAuthUser(pb.authStore.model?.verified === true);
}
};
checkOAuthStatus();
}, [userId]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setStatus({ message: '', isError: false });
try {
// Validate inputs
if (newPassword !== confirmPassword) {
throw new Error("New passwords don't match");
}
if (newPassword.length < 8) {
throw new Error("Password must be at least 8 characters");
}
// SPECIAL HANDLING FOR OAUTH USERS
if (isOAuthUser) {
// For OAuth users, we need to use a different endpoint or method
// since the regular update requires oldPassword
const authData = await pb.collection('users').update(userId, {
password: newPassword,
passwordConfirm: confirmPassword
}, {
// This header might be needed depending on your PocketBase version
headers: { 'X-Require-Password-Confirm': 'false' }
});
} else {
// Regular password update flow
await pb.collection('users').update(userId, {
password: newPassword,
passwordConfirm: confirmPassword,
oldPassword: currentPassword
});
}
// Clear form
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
setStatus({
message: isOAuthUser
? 'Password successfully set! You can now login with email'
: 'Password updated successfully!',
isError: false
});
// Logout user after password change
onLogout();
} catch (error: any) {
console.error("Password update failed:", error);
// Special handling for OAuth users if the first method fails
if (isOAuthUser && error.data?.oldPassword?.message) {
try {
// Alternative method for OAuth users - create a new auth record
await pb.collection('users').authWithPassword(
pb.authStore.model?.email,
newPassword
);
setStatus({
message: 'Password successfully set!',
isError: false
});
onLogout();
return;
} catch (secondError) {
error = secondError;
}
}
// Handle specific PocketBase validation errors
let errorMessage = error.message;
if (error.data?.oldPassword?.message) {
errorMessage = "Please provide your current password";
} else if (error.data?.password?.message) {
errorMessage = error.data.password.message;
}
setStatus({
message: errorMessage || "Failed to update password. Please try again.",
isError: true
});
} finally {
setIsLoading(false);
}
};
return (
<Card>
<CardHeader>
<CardTitle>Security</CardTitle>
<CardDescription>
{isOAuthUser
? "Set a password to enable email login"
: "Update your password"}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<form onSubmit={handleSubmit}>
{status.message && (
<div className={`p-3 rounded-md text-sm mb-4 ${status.isError ? 'bg-red-50 text-red-600' : 'bg-green-50 text-green-600'}`}>
{status.message}
</div>
)}
{!isOAuthUser && (
<div className="space-y-2">
<Label htmlFor="currentPassword">Current password</Label>
<div className="relative">
<Input
id="currentPassword"
type={showCurrentPassword ? "text" : "password"}
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
required={!isOAuthUser}
/>
<button
type="button"
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showCurrentPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
)}
<div className="space-y-2">
<Label htmlFor="newPassword">
{isOAuthUser ? "Set new password" : "New password"}
</Label>
<div className="relative">
<Input
id="newPassword"
type={showNewPassword ? "text" : "password"}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
minLength={8}
/>
<button
type="button"
onClick={() => setShowNewPassword(!showNewPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showNewPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
<p className="text-xs text-muted-foreground">
Password must be at least 8 characters
</p>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">
{isOAuthUser ? "Confirm new password" : "Confirm password"}
</Label>
<div className="relative">
<Input
id="confirmPassword"
type={showConfirmPassword ? "text" : "password"}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
minLength={8}
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showConfirmPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
<Button
type="submit"
className="mt-4 w-full"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{isOAuthUser ? "Setting password..." : "Updating..."}
</>
) : (
isOAuthUser ? "Set Password" : "Update Password"
)}
</Button>
</form>
</CardContent>
</Card>
);
};
export default PasswordUpdateCard;