287 lines
9.6 KiB
TypeScript
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; |