Files
agentic-coding-starter-kit/create-agentic-app/template/src/app/profile/page.tsx
Auto f29d296816 feat: replace Google OAuth with email/password authentication
Replace Google OAuth provider with email/password authentication to reduce
friction for MVP development and vibe coding workflows.

Changes:
- Remove Google OAuth configuration from auth.ts
- Add emailAndPassword provider with enabled: true
- Add email verification with sendOnSignUp: true
- Add password reset functionality
- Log verification and reset URLs to terminal (no email integration yet)

New auth pages (src/app/(auth)/):
- /login - Sign in page
- /register - Sign up page
- /forgot-password - Password reset request
- /reset-password - Password reset completion

New components (src/components/auth/):
- sign-up-form.tsx - Registration form
- forgot-password-form.tsx - Password reset request form
- reset-password-form.tsx - Password reset form

Updated components:
- sign-in-button.tsx - Now email/password form instead of Google button
- user-profile.tsx - Shows Sign in/Sign up buttons when logged out

Bug fixes:
- Fix React render error in profile page by wrapping router.push in useEffect

Config updates:
- Remove GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET from env.example
- Update CLAUDE.md documentation to reflect email/password auth
- Add requestPasswordReset, resetPassword, sendVerificationEmail to auth-client exports

All changes applied to both main project and create-agentic-app template.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 05:50:43 +02:00

417 lines
15 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { Mail, Calendar, User, Shield, ArrowLeft, Lock, Smartphone } from "lucide-react";
import { toast } from "sonner";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { useSession } from "@/lib/auth-client";
export default function ProfilePage() {
const { data: session, isPending } = useSession();
const router = useRouter();
const [editProfileOpen, setEditProfileOpen] = useState(false);
const [securityOpen, setSecurityOpen] = useState(false);
const [emailPrefsOpen, setEmailPrefsOpen] = useState(false);
useEffect(() => {
if (!isPending && !session) {
router.push("/");
}
}, [isPending, session, router]);
if (isPending || !session) {
return (
<div className="flex items-center justify-center min-h-screen">
<div>Loading...</div>
</div>
);
}
const user = session.user;
const createdDate = user.createdAt
? new Date(user.createdAt).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})
: null;
const handleEditProfileSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// In a real app, this would call an API to update the user profile
toast.info("Profile updates require backend implementation");
setEditProfileOpen(false);
};
return (
<div className="container max-w-4xl mx-auto py-8 px-4">
<div className="flex items-center gap-4 mb-8">
<Button
variant="ghost"
size="sm"
onClick={() => router.back()}
className="flex items-center gap-2"
>
<ArrowLeft className="h-4 w-4" />
Back
</Button>
<h1 className="text-3xl font-bold">Your Profile</h1>
</div>
<div className="grid gap-6">
{/* Profile Overview Card */}
<Card>
<CardHeader>
<div className="flex items-center space-x-4">
<Avatar className="h-20 w-20">
<AvatarImage
src={user.image || ""}
alt={user.name || "User"}
referrerPolicy="no-referrer"
/>
<AvatarFallback className="text-lg">
{(user.name?.[0] || user.email?.[0] || "U").toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="space-y-2">
<h2 className="text-2xl font-semibold">{user.name}</h2>
<div className="flex items-center gap-2 text-muted-foreground">
<Mail className="h-4 w-4" />
<span>{user.email}</span>
{user.emailVerified && (
<Badge
variant="outline"
className="text-green-600 border-green-600"
>
<Shield className="h-3 w-3 mr-1" />
Verified
</Badge>
)}
</div>
{createdDate && (
<div className="flex items-center gap-2 text-muted-foreground text-sm">
<Calendar className="h-4 w-4" />
<span>Member since {createdDate}</span>
</div>
)}
</div>
</div>
</CardHeader>
</Card>
{/* Account Information */}
<Card>
<CardHeader>
<CardTitle>Account Information</CardTitle>
<CardDescription>Your account details and settings</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-muted-foreground">
Full Name
</label>
<div className="p-3 border rounded-md bg-muted/10">
{user.name || "Not provided"}
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-muted-foreground">
Email Address
</label>
<div className="p-3 border rounded-md bg-muted/10 flex items-center justify-between">
<span>{user.email}</span>
{user.emailVerified && (
<Badge
variant="outline"
className="text-green-600 border-green-600"
>
Verified
</Badge>
)}
</div>
</div>
</div>
<Separator />
<div className="space-y-4">
<h3 className="text-lg font-medium">Account Status</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="space-y-1">
<p className="font-medium">Email Verification</p>
<p className="text-sm text-muted-foreground">
Email address verification status
</p>
</div>
<Badge variant={user.emailVerified ? "default" : "secondary"}>
{user.emailVerified ? "Verified" : "Unverified"}
</Badge>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="space-y-1">
<p className="font-medium">Account Type</p>
<p className="text-sm text-muted-foreground">
Your account access level
</p>
</div>
<Badge variant="outline">Standard</Badge>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Account Activity */}
<Card>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
<CardDescription>
Your recent account activity and sessions
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center space-x-3">
<div className="h-2 w-2 bg-green-500 rounded-full"></div>
<div>
<p className="font-medium">Current Session</p>
<p className="text-sm text-muted-foreground">Active now</p>
</div>
</div>
<Badge
variant="outline"
className="text-green-600 border-green-600"
>
Active
</Badge>
</div>
</div>
</CardContent>
</Card>
{/* Quick Actions */}
<Card>
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
<CardDescription>
Manage your account settings and preferences
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Button
variant="outline"
className="justify-start h-auto p-4"
onClick={() => setEditProfileOpen(true)}
>
<User className="h-4 w-4 mr-2" />
<div className="text-left">
<div className="font-medium">Edit Profile</div>
<div className="text-xs text-muted-foreground">
Update your information
</div>
</div>
</Button>
<Button
variant="outline"
className="justify-start h-auto p-4"
onClick={() => setSecurityOpen(true)}
>
<Shield className="h-4 w-4 mr-2" />
<div className="text-left">
<div className="font-medium">Security Settings</div>
<div className="text-xs text-muted-foreground">
Manage security options
</div>
</div>
</Button>
<Button
variant="outline"
className="justify-start h-auto p-4"
onClick={() => setEmailPrefsOpen(true)}
>
<Mail className="h-4 w-4 mr-2" />
<div className="text-left">
<div className="font-medium">Email Preferences</div>
<div className="text-xs text-muted-foreground">
Configure notifications
</div>
</div>
</Button>
</div>
</CardContent>
</Card>
</div>
{/* Edit Profile Dialog */}
<Dialog open={editProfileOpen} onOpenChange={setEditProfileOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription>
Update your profile information. Changes will be saved to your
account.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleEditProfileSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Full Name</Label>
<Input
id="name"
defaultValue={user.name || ""}
placeholder="Enter your name"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
defaultValue={user.email || ""}
disabled
className="bg-muted"
/>
<p className="text-xs text-muted-foreground">
Email cannot be changed for OAuth accounts
</p>
</div>
<div className="flex justify-end gap-2 pt-4">
<Button
type="button"
variant="outline"
onClick={() => setEditProfileOpen(false)}
>
Cancel
</Button>
<Button type="submit">Save Changes</Button>
</div>
</form>
</DialogContent>
</Dialog>
{/* Security Settings Dialog */}
<Dialog open={securityOpen} onOpenChange={setSecurityOpen}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Security Settings</DialogTitle>
<DialogDescription>
Manage your account security and authentication options.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center gap-3">
<Lock className="h-5 w-5 text-muted-foreground" />
<div>
<p className="font-medium">Password</p>
<p className="text-sm text-muted-foreground">
{user.email?.includes("@gmail")
? "Managed by Google"
: "Set a password for your account"}
</p>
</div>
</div>
<Badge variant="outline">
{user.email?.includes("@gmail") ? "OAuth" : "Not Set"}
</Badge>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center gap-3">
<Smartphone className="h-5 w-5 text-muted-foreground" />
<div>
<p className="font-medium">Two-Factor Authentication</p>
<p className="text-sm text-muted-foreground">
Add an extra layer of security
</p>
</div>
</div>
<Button variant="outline" size="sm" disabled>
Coming Soon
</Button>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center gap-3">
<Shield className="h-5 w-5 text-muted-foreground" />
<div>
<p className="font-medium">Active Sessions</p>
<p className="text-sm text-muted-foreground">
Manage devices logged into your account
</p>
</div>
</div>
<Badge variant="default">1 Active</Badge>
</div>
</div>
<div className="flex justify-end pt-4">
<Button variant="outline" onClick={() => setSecurityOpen(false)}>
Close
</Button>
</div>
</DialogContent>
</Dialog>
{/* Email Preferences Dialog */}
<Dialog open={emailPrefsOpen} onOpenChange={setEmailPrefsOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Email Preferences</DialogTitle>
<DialogDescription>
Configure your email notification settings.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<p className="font-medium">Marketing Emails</p>
<p className="text-sm text-muted-foreground">
Product updates and announcements
</p>
</div>
<Badge variant="secondary">Coming Soon</Badge>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<p className="font-medium">Security Alerts</p>
<p className="text-sm text-muted-foreground">
Important security notifications
</p>
</div>
<Badge variant="default">Always On</Badge>
</div>
</div>
<div className="flex justify-end pt-4">
<Button variant="outline" onClick={() => setEmailPrefsOpen(false)}>
Close
</Button>
</div>
</DialogContent>
</Dialog>
</div>
);
}