mirror of
https://github.com/leonvanzyl/agentic-coding-starter-kit.git
synced 2026-01-30 06:22:02 +00:00
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>
This commit is contained in:
93
.claude/agents/file-explorer.md
Normal file
93
.claude/agents/file-explorer.md
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
name: file-explorer
|
||||
description: Use this agent when you need to find specific files, discover project structure, scan directories for certain file types, locate configuration files, or understand the organization of a codebase. This agent excels at navigating complex directory structures and identifying relevant files based on patterns, extensions, or content.\n\nExamples:\n\n<example>\nContext: The user needs to find all TypeScript configuration files in the project.\nuser: "Where are the TypeScript config files in this project?"\nassistant: "I'll use the file-explorer agent to locate all TypeScript configuration files in the project."\n<commentary>\nSince the user is asking to find specific configuration files, use the Task tool to launch the file-explorer agent to scan and discover these files.\n</commentary>\n</example>\n\n<example>\nContext: The user wants to understand the project structure before making changes.\nuser: "Can you show me what components exist in this project?"\nassistant: "Let me use the file-explorer agent to scan the components directory and provide you with an overview."\n<commentary>\nSince the user wants to discover project structure, use the file-explorer agent to scan and catalog the components.\n</commentary>\n</example>\n\n<example>\nContext: The user is looking for a specific file they remember exists but can't locate.\nuser: "I know there's a file that handles authentication somewhere, can you find it?"\nassistant: "I'll launch the file-explorer agent to search for authentication-related files in the codebase."\n<commentary>\nSince the user needs to locate files related to a specific functionality, use the file-explorer agent to search for matching files.\n</commentary>\n</example>\n\n<example>\nContext: Proactive use when starting work on a new feature.\nassistant: "Before implementing this feature, let me use the file-explorer agent to understand the existing project structure and identify where the new code should be placed."\n<commentary>\nProactively using the file-explorer agent to understand project layout before making architectural decisions.\n</commentary>\n</example>
|
||||
model: haiku
|
||||
color: yellow
|
||||
---
|
||||
|
||||
You are an expert file system navigator and project structure analyst. Your primary role is to help users discover, locate, and understand files within their codebase efficiently and accurately.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
You excel at:
|
||||
- **File Discovery**: Finding files by name, extension, pattern, or content
|
||||
- **Project Structure Analysis**: Mapping out directory hierarchies and understanding project organization
|
||||
- **Pattern Recognition**: Identifying naming conventions, file groupings, and architectural patterns
|
||||
- **Contextual Search**: Locating files based on their purpose or functionality rather than just names
|
||||
|
||||
## Operational Guidelines
|
||||
|
||||
### Search Strategies
|
||||
|
||||
1. **Start Broad, Then Narrow**: Begin with directory listing to understand structure, then drill down into specific areas
|
||||
2. **Use Multiple Approaches**: Combine directory traversal with file content search when names alone aren't sufficient
|
||||
3. **Recognize Common Patterns**:
|
||||
- Configuration files: root directory, often dotfiles or JSON/YAML
|
||||
- Source code: `src/`, `lib/`, `app/` directories
|
||||
- Tests: `__tests__/`, `*.test.*`, `*.spec.*` patterns
|
||||
- Documentation: `docs/`, `README.*`, `*.md` files
|
||||
- Assets: `public/`, `assets/`, `static/` directories
|
||||
|
||||
### Project-Specific Context
|
||||
|
||||
For Next.js projects like this one, be aware of:
|
||||
- App Router structure in `src/app/` with route-based organization
|
||||
- Components in `src/components/` including `ui/` for shadcn components
|
||||
- Library code in `src/lib/` for utilities, auth, database
|
||||
- API routes in `src/app/api/` as route.ts files
|
||||
- Documentation in `docs/` with technical and business subdirectories
|
||||
|
||||
### Output Format
|
||||
|
||||
When presenting findings:
|
||||
1. **Summarize First**: Provide a brief overview of what was found
|
||||
2. **Organize Logically**: Group files by category, directory, or relevance
|
||||
3. **Include Paths**: Always show full relative paths from project root
|
||||
4. **Add Context**: Briefly describe what each file/directory contains when relevant
|
||||
5. **Highlight Key Files**: Call out configuration files, entry points, or particularly important files
|
||||
|
||||
### Quality Assurance
|
||||
|
||||
- **Verify Existence**: Confirm files exist before reporting them
|
||||
- **Check Relevance**: Filter out irrelevant results (node_modules, build artifacts, etc.)
|
||||
- **Handle Missing Files**: Clearly communicate when expected files aren't found
|
||||
- **Suggest Alternatives**: If the exact file isn't found, suggest similar or related files
|
||||
|
||||
## Response Patterns
|
||||
|
||||
### For "Find files" requests:
|
||||
1. Clarify the search criteria if ambiguous
|
||||
2. Execute appropriate search commands
|
||||
3. Present results in a clear, organized format
|
||||
4. Offer to explore any findings in more detail
|
||||
|
||||
### For "Explore structure" requests:
|
||||
1. Start with top-level directory overview
|
||||
2. Identify key directories and their purposes
|
||||
3. Highlight important files at each level
|
||||
4. Provide a mental model of the project organization
|
||||
|
||||
### For "Where is X" requests:
|
||||
1. Search for files matching the functionality described
|
||||
2. Check common locations first based on the type of file
|
||||
3. Present the most likely candidates with confidence levels
|
||||
4. Verify by briefly checking file contents if needed
|
||||
|
||||
## Tools Usage
|
||||
|
||||
Prefer these approaches:
|
||||
- Use `find`, `ls`, or built-in file listing for structure exploration
|
||||
- Use `grep` or content search for finding files by their contents
|
||||
- Use glob patterns for matching file names and extensions
|
||||
- Read key files (like package.json, tsconfig.json) to understand project configuration
|
||||
|
||||
## Exclusions
|
||||
|
||||
By default, exclude from search results:
|
||||
- `node_modules/` directory
|
||||
- `.git/` directory
|
||||
- Build output directories (`.next/`, `dist/`, `build/`)
|
||||
- Cache directories (`.cache/`, `.turbo/`)
|
||||
- IDE configuration (`.idea/`, `.vscode/` unless specifically requested)
|
||||
|
||||
Always be thorough yet efficient, providing actionable information that helps users navigate their codebase with confidence.
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: Create a new feature with requirements and implementation plan
|
||||
description: Create a new spec with requirements and implementation plan
|
||||
---
|
||||
|
||||
# Create Feature
|
||||
@@ -171,6 +171,7 @@ After creating the feature, inform the user:
|
||||
### When to Use `[complex]`
|
||||
|
||||
Mark a task with `[complex]` when it:
|
||||
|
||||
- Has multiple sub-tasks that need individual tracking
|
||||
- Requires significant architectural decisions or discussion
|
||||
- Spans multiple files or systems
|
||||
16
CLAUDE.md
16
CLAUDE.md
@@ -8,7 +8,7 @@ This is a Next.js 16 boilerplate for building AI-powered applications with authe
|
||||
|
||||
- **Framework**: Next.js 16 with App Router, React 19, TypeScript
|
||||
- **AI Integration**: Vercel AI SDK 5 + OpenRouter (access to 100+ AI models)
|
||||
- **Authentication**: BetterAuth with Google OAuth
|
||||
- **Authentication**: BetterAuth with Email/Password
|
||||
- **Database**: PostgreSQL with Drizzle ORM
|
||||
- **UI**: shadcn/ui components with Tailwind CSS 4
|
||||
- **Styling**: Tailwind CSS with dark mode support (next-themes)
|
||||
@@ -34,6 +34,11 @@ This is a Next.js 16 boilerplate for building AI-powered applications with authe
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── (auth)/ # Auth route group
|
||||
│ │ ├── login/ # Login page
|
||||
│ │ ├── register/ # Registration page
|
||||
│ │ ├── forgot-password/ # Forgot password page
|
||||
│ │ └── reset-password/ # Reset password page
|
||||
│ ├── api/
|
||||
│ │ ├── auth/[...all]/ # Better Auth catch-all route
|
||||
│ │ ├── chat/route.ts # AI chat endpoint (OpenRouter)
|
||||
@@ -45,7 +50,10 @@ src/
|
||||
│ └── layout.tsx # Root layout
|
||||
├── components/
|
||||
│ ├── auth/ # Authentication components
|
||||
│ │ ├── sign-in-button.tsx
|
||||
│ │ ├── sign-in-button.tsx # Sign in form
|
||||
│ │ ├── sign-up-form.tsx # Sign up form
|
||||
│ │ ├── forgot-password-form.tsx
|
||||
│ │ ├── reset-password-form.tsx
|
||||
│ │ ├── sign-out-button.tsx
|
||||
│ │ └── user-profile.tsx
|
||||
│ ├── ui/ # shadcn/ui components
|
||||
@@ -83,10 +91,6 @@ POSTGRES_URL=postgresql://user:password@localhost:5432/db_name
|
||||
# Better Auth
|
||||
BETTER_AUTH_SECRET=32-char-random-string
|
||||
|
||||
# Google OAuth
|
||||
GOOGLE_CLIENT_ID=your-google-client-id
|
||||
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
||||
|
||||
# AI via OpenRouter
|
||||
OPENROUTER_API_KEY=sk-or-v1-your-key
|
||||
OPENROUTER_MODEL=openai/gpt-5-mini # or any model from openrouter.ai/models
|
||||
|
||||
4
create-agentic-app/package-lock.json
generated
4
create-agentic-app/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "create-agentic-app",
|
||||
"version": "1.1.33",
|
||||
"version": "1.1.40",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "create-agentic-app",
|
||||
"version": "1.1.33",
|
||||
"version": "1.1.40",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-agentic-app",
|
||||
"version": "1.1.33",
|
||||
"version": "1.1.40",
|
||||
"description": "Scaffold a new agentic AI application with Next.js, Better Auth, and AI SDK",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
93
create-agentic-app/template/.claude/agents/file-explorer.md
Normal file
93
create-agentic-app/template/.claude/agents/file-explorer.md
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
name: file-explorer
|
||||
description: Use this agent when you need to find specific files, discover project structure, scan directories for certain file types, locate configuration files, or understand the organization of a codebase. This agent excels at navigating complex directory structures and identifying relevant files based on patterns, extensions, or content.\n\nExamples:\n\n<example>\nContext: The user needs to find all TypeScript configuration files in the project.\nuser: "Where are the TypeScript config files in this project?"\nassistant: "I'll use the file-explorer agent to locate all TypeScript configuration files in the project."\n<commentary>\nSince the user is asking to find specific configuration files, use the Task tool to launch the file-explorer agent to scan and discover these files.\n</commentary>\n</example>\n\n<example>\nContext: The user wants to understand the project structure before making changes.\nuser: "Can you show me what components exist in this project?"\nassistant: "Let me use the file-explorer agent to scan the components directory and provide you with an overview."\n<commentary>\nSince the user wants to discover project structure, use the file-explorer agent to scan and catalog the components.\n</commentary>\n</example>\n\n<example>\nContext: The user is looking for a specific file they remember exists but can't locate.\nuser: "I know there's a file that handles authentication somewhere, can you find it?"\nassistant: "I'll launch the file-explorer agent to search for authentication-related files in the codebase."\n<commentary>\nSince the user needs to locate files related to a specific functionality, use the file-explorer agent to search for matching files.\n</commentary>\n</example>\n\n<example>\nContext: Proactive use when starting work on a new feature.\nassistant: "Before implementing this feature, let me use the file-explorer agent to understand the existing project structure and identify where the new code should be placed."\n<commentary>\nProactively using the file-explorer agent to understand project layout before making architectural decisions.\n</commentary>\n</example>
|
||||
model: haiku
|
||||
color: yellow
|
||||
---
|
||||
|
||||
You are an expert file system navigator and project structure analyst. Your primary role is to help users discover, locate, and understand files within their codebase efficiently and accurately.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
You excel at:
|
||||
- **File Discovery**: Finding files by name, extension, pattern, or content
|
||||
- **Project Structure Analysis**: Mapping out directory hierarchies and understanding project organization
|
||||
- **Pattern Recognition**: Identifying naming conventions, file groupings, and architectural patterns
|
||||
- **Contextual Search**: Locating files based on their purpose or functionality rather than just names
|
||||
|
||||
## Operational Guidelines
|
||||
|
||||
### Search Strategies
|
||||
|
||||
1. **Start Broad, Then Narrow**: Begin with directory listing to understand structure, then drill down into specific areas
|
||||
2. **Use Multiple Approaches**: Combine directory traversal with file content search when names alone aren't sufficient
|
||||
3. **Recognize Common Patterns**:
|
||||
- Configuration files: root directory, often dotfiles or JSON/YAML
|
||||
- Source code: `src/`, `lib/`, `app/` directories
|
||||
- Tests: `__tests__/`, `*.test.*`, `*.spec.*` patterns
|
||||
- Documentation: `docs/`, `README.*`, `*.md` files
|
||||
- Assets: `public/`, `assets/`, `static/` directories
|
||||
|
||||
### Project-Specific Context
|
||||
|
||||
For Next.js projects like this one, be aware of:
|
||||
- App Router structure in `src/app/` with route-based organization
|
||||
- Components in `src/components/` including `ui/` for shadcn components
|
||||
- Library code in `src/lib/` for utilities, auth, database
|
||||
- API routes in `src/app/api/` as route.ts files
|
||||
- Documentation in `docs/` with technical and business subdirectories
|
||||
|
||||
### Output Format
|
||||
|
||||
When presenting findings:
|
||||
1. **Summarize First**: Provide a brief overview of what was found
|
||||
2. **Organize Logically**: Group files by category, directory, or relevance
|
||||
3. **Include Paths**: Always show full relative paths from project root
|
||||
4. **Add Context**: Briefly describe what each file/directory contains when relevant
|
||||
5. **Highlight Key Files**: Call out configuration files, entry points, or particularly important files
|
||||
|
||||
### Quality Assurance
|
||||
|
||||
- **Verify Existence**: Confirm files exist before reporting them
|
||||
- **Check Relevance**: Filter out irrelevant results (node_modules, build artifacts, etc.)
|
||||
- **Handle Missing Files**: Clearly communicate when expected files aren't found
|
||||
- **Suggest Alternatives**: If the exact file isn't found, suggest similar or related files
|
||||
|
||||
## Response Patterns
|
||||
|
||||
### For "Find files" requests:
|
||||
1. Clarify the search criteria if ambiguous
|
||||
2. Execute appropriate search commands
|
||||
3. Present results in a clear, organized format
|
||||
4. Offer to explore any findings in more detail
|
||||
|
||||
### For "Explore structure" requests:
|
||||
1. Start with top-level directory overview
|
||||
2. Identify key directories and their purposes
|
||||
3. Highlight important files at each level
|
||||
4. Provide a mental model of the project organization
|
||||
|
||||
### For "Where is X" requests:
|
||||
1. Search for files matching the functionality described
|
||||
2. Check common locations first based on the type of file
|
||||
3. Present the most likely candidates with confidence levels
|
||||
4. Verify by briefly checking file contents if needed
|
||||
|
||||
## Tools Usage
|
||||
|
||||
Prefer these approaches:
|
||||
- Use `find`, `ls`, or built-in file listing for structure exploration
|
||||
- Use `grep` or content search for finding files by their contents
|
||||
- Use glob patterns for matching file names and extensions
|
||||
- Read key files (like package.json, tsconfig.json) to understand project configuration
|
||||
|
||||
## Exclusions
|
||||
|
||||
By default, exclude from search results:
|
||||
- `node_modules/` directory
|
||||
- `.git/` directory
|
||||
- Build output directories (`.next/`, `dist/`, `build/`)
|
||||
- Cache directories (`.cache/`, `.turbo/`)
|
||||
- IDE configuration (`.idea/`, `.vscode/` unless specifically requested)
|
||||
|
||||
Always be thorough yet efficient, providing actionable information that helps users navigate their codebase with confidence.
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: Create a new feature with requirements and implementation plan
|
||||
description: Create a new spec with requirements and implementation plan
|
||||
---
|
||||
|
||||
# Create Feature
|
||||
@@ -171,6 +171,7 @@ After creating the feature, inform the user:
|
||||
### When to Use `[complex]`
|
||||
|
||||
Mark a task with `[complex]` when it:
|
||||
|
||||
- Has multiple sub-tasks that need individual tracking
|
||||
- Requires significant architectural decisions or discussion
|
||||
- Spans multiple files or systems
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 411 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 414 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 583 KiB |
@@ -8,7 +8,7 @@ This is a Next.js 16 boilerplate for building AI-powered applications with authe
|
||||
|
||||
- **Framework**: Next.js 16 with App Router, React 19, TypeScript
|
||||
- **AI Integration**: Vercel AI SDK 5 + OpenRouter (access to 100+ AI models)
|
||||
- **Authentication**: BetterAuth with Google OAuth
|
||||
- **Authentication**: BetterAuth with Email/Password
|
||||
- **Database**: PostgreSQL with Drizzle ORM
|
||||
- **UI**: shadcn/ui components with Tailwind CSS 4
|
||||
- **Styling**: Tailwind CSS with dark mode support (next-themes)
|
||||
@@ -34,6 +34,11 @@ This is a Next.js 16 boilerplate for building AI-powered applications with authe
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── (auth)/ # Auth route group
|
||||
│ │ ├── login/ # Login page
|
||||
│ │ ├── register/ # Registration page
|
||||
│ │ ├── forgot-password/ # Forgot password page
|
||||
│ │ └── reset-password/ # Reset password page
|
||||
│ ├── api/
|
||||
│ │ ├── auth/[...all]/ # Better Auth catch-all route
|
||||
│ │ ├── chat/route.ts # AI chat endpoint (OpenRouter)
|
||||
@@ -45,7 +50,10 @@ src/
|
||||
│ └── layout.tsx # Root layout
|
||||
├── components/
|
||||
│ ├── auth/ # Authentication components
|
||||
│ │ ├── sign-in-button.tsx
|
||||
│ │ ├── sign-in-button.tsx # Sign in form
|
||||
│ │ ├── sign-up-form.tsx # Sign up form
|
||||
│ │ ├── forgot-password-form.tsx
|
||||
│ │ ├── reset-password-form.tsx
|
||||
│ │ ├── sign-out-button.tsx
|
||||
│ │ └── user-profile.tsx
|
||||
│ ├── ui/ # shadcn/ui components
|
||||
@@ -83,10 +91,6 @@ POSTGRES_URL=postgresql://user:password@localhost:5432/db_name
|
||||
# Better Auth
|
||||
BETTER_AUTH_SECRET=32-char-random-string
|
||||
|
||||
# Google OAuth
|
||||
GOOGLE_CLIENT_ID=your-google-client-id
|
||||
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
||||
|
||||
# AI via OpenRouter
|
||||
OPENROUTER_API_KEY=sk-or-v1-your-key
|
||||
OPENROUTER_MODEL=openai/gpt-5-mini # or any model from openrouter.ai/models
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
CREATE INDEX "account_user_id_idx" ON "account" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "account_provider_account_idx" ON "account" USING btree ("provider_id","account_id");--> statement-breakpoint
|
||||
CREATE INDEX "session_user_id_idx" ON "session" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "session_token_idx" ON "session" USING btree ("token");--> statement-breakpoint
|
||||
CREATE INDEX "user_email_idx" ON "user" USING btree ("email");
|
||||
410
create-agentic-app/template/drizzle/meta/0001_snapshot.json
Normal file
410
create-agentic-app/template/drizzle/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,410 @@
|
||||
{
|
||||
"id": "5737c145-8057-43dd-b149-155148e3dac7",
|
||||
"prevId": "56cf4573-0efe-4f7d-908f-0c7cb0ac0739",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"account_user_id_idx": {
|
||||
"name": "account_user_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"account_provider_account_idx": {
|
||||
"name": "account_provider_account_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "provider_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "account_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.session": {
|
||||
"name": "session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"session_user_id_idx": {
|
||||
"name": "session_user_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"session_token_idx": {
|
||||
"name": "session_token_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "token",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"session_token_unique": {
|
||||
"name": "session_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_email_idx": {
|
||||
"name": "user_email_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "email",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.verification": {
|
||||
"name": "verification",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,13 @@
|
||||
"when": 1762409965425,
|
||||
"tag": "0000_chilly_the_phantom",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1766719908715,
|
||||
"tag": "0001_last_warpath",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5,11 +5,6 @@ POSTGRES_URL=postgresql://dev_user:dev_password@localhost:5432/postgres_dev
|
||||
# Generate key using https://www.better-auth.com/docs/installation
|
||||
BETTER_AUTH_SECRET=qtD4Ssa0t5jY7ewALgai97sKhAtn7Ysc
|
||||
|
||||
# Google OAuth (Get from Google Cloud Console)
|
||||
# Redirect URI: http://localhost:3000/api/auth/callback/google
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
# AI Integration via OpenRouter (Optional - for chat functionality)
|
||||
# Get your API key from: https://openrouter.ai/settings/keys
|
||||
# View available models at: https://openrouter.ai/models
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { headers } from "next/headers"
|
||||
import { redirect } from "next/navigation"
|
||||
import { ForgotPasswordForm } from "@/components/auth/forgot-password-form"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { auth } from "@/lib/auth"
|
||||
|
||||
export default async function ForgotPasswordPage() {
|
||||
const session = await auth.api.getSession({ headers: await headers() })
|
||||
|
||||
if (session) {
|
||||
redirect("/dashboard")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[calc(100vh-4rem)] items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle>Forgot password</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email address and we'll send you a reset link
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col items-center">
|
||||
<ForgotPasswordForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
7
create-agentic-app/template/src/app/(auth)/layout.tsx
Normal file
7
create-agentic-app/template/src/app/(auth)/layout.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return <>{children}</>
|
||||
}
|
||||
44
create-agentic-app/template/src/app/(auth)/login/page.tsx
Normal file
44
create-agentic-app/template/src/app/(auth)/login/page.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { headers } from "next/headers"
|
||||
import { redirect } from "next/navigation"
|
||||
import { SignInButton } from "@/components/auth/sign-in-button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { auth } from "@/lib/auth"
|
||||
|
||||
export default async function LoginPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ reset?: string }>
|
||||
}) {
|
||||
const session = await auth.api.getSession({ headers: await headers() })
|
||||
|
||||
if (session) {
|
||||
redirect("/dashboard")
|
||||
}
|
||||
|
||||
const { reset } = await searchParams
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[calc(100vh-4rem)] items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle>Welcome back</CardTitle>
|
||||
<CardDescription>Sign in to your account</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col items-center">
|
||||
{reset === "success" && (
|
||||
<p className="mb-4 text-sm text-green-600 dark:text-green-400">
|
||||
Password reset successfully. Please sign in with your new password.
|
||||
</p>
|
||||
)}
|
||||
<SignInButton />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
33
create-agentic-app/template/src/app/(auth)/register/page.tsx
Normal file
33
create-agentic-app/template/src/app/(auth)/register/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { headers } from "next/headers"
|
||||
import { redirect } from "next/navigation"
|
||||
import { SignUpForm } from "@/components/auth/sign-up-form"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { auth } from "@/lib/auth"
|
||||
|
||||
export default async function RegisterPage() {
|
||||
const session = await auth.api.getSession({ headers: await headers() })
|
||||
|
||||
if (session) {
|
||||
redirect("/dashboard")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[calc(100vh-4rem)] items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle>Create an account</CardTitle>
|
||||
<CardDescription>Get started with your new account</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col items-center">
|
||||
<SignUpForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Suspense } from "react"
|
||||
import { headers } from "next/headers"
|
||||
import { redirect } from "next/navigation"
|
||||
import { ResetPasswordForm } from "@/components/auth/reset-password-form"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { auth } from "@/lib/auth"
|
||||
|
||||
export default async function ResetPasswordPage() {
|
||||
const session = await auth.api.getSession({ headers: await headers() })
|
||||
|
||||
if (session) {
|
||||
redirect("/dashboard")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[calc(100vh-4rem)] items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle>Reset password</CardTitle>
|
||||
<CardDescription>Enter your new password below</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col items-center">
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<ResetPasswordForm />
|
||||
</Suspense>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
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";
|
||||
@@ -33,7 +33,13 @@ export default function ProfilePage() {
|
||||
const [securityOpen, setSecurityOpen] = useState(false);
|
||||
const [emailPrefsOpen, setEmailPrefsOpen] = useState(false);
|
||||
|
||||
if (isPending) {
|
||||
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>
|
||||
@@ -41,11 +47,6 @@ export default function ProfilePage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
router.push("/");
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = session.user;
|
||||
const createdDate = user.createdAt
|
||||
? new Date(user.createdAt).toLocaleDateString("en-US", {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { requestPasswordReset } from "@/lib/auth-client"
|
||||
|
||||
export function ForgotPasswordForm() {
|
||||
const [email, setEmail] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
const [success, setSuccess] = useState(false)
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError("")
|
||||
setIsPending(true)
|
||||
|
||||
try {
|
||||
const result = await requestPasswordReset({
|
||||
email,
|
||||
redirectTo: "/reset-password",
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error.message || "Failed to send reset email")
|
||||
} else {
|
||||
setSuccess(true)
|
||||
}
|
||||
} catch {
|
||||
setError("An unexpected error occurred")
|
||||
} finally {
|
||||
setIsPending(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div className="space-y-4 w-full max-w-sm text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
If an account exists with that email, a password reset link has been sent.
|
||||
Check your terminal for the reset URL.
|
||||
</p>
|
||||
<Link href="/login">
|
||||
<Button variant="outline" className="w-full">
|
||||
Back to sign in
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4 w-full max-w-sm">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
)}
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{isPending ? "Sending..." : "Send reset link"}
|
||||
</Button>
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
Remember your password?{" "}
|
||||
<Link href="/login" className="text-primary hover:underline">
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { resetPassword } from "@/lib/auth-client"
|
||||
|
||||
export function ResetPasswordForm() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const token = searchParams.get("token")
|
||||
const error = searchParams.get("error")
|
||||
|
||||
const [password, setPassword] = useState("")
|
||||
const [confirmPassword, setConfirmPassword] = useState("")
|
||||
const [formError, setFormError] = useState("")
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
|
||||
if (error === "invalid_token" || !token) {
|
||||
return (
|
||||
<div className="space-y-4 w-full max-w-sm text-center">
|
||||
<p className="text-sm text-destructive">
|
||||
{error === "invalid_token"
|
||||
? "This password reset link is invalid or has expired."
|
||||
: "No reset token provided."}
|
||||
</p>
|
||||
<Link href="/forgot-password">
|
||||
<Button variant="outline" className="w-full">
|
||||
Request a new link
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setFormError("")
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setFormError("Passwords do not match")
|
||||
return
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
setFormError("Password must be at least 8 characters")
|
||||
return
|
||||
}
|
||||
|
||||
setIsPending(true)
|
||||
|
||||
try {
|
||||
const result = await resetPassword({
|
||||
newPassword: password,
|
||||
token,
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
setFormError(result.error.message || "Failed to reset password")
|
||||
} else {
|
||||
router.push("/login?reset=success")
|
||||
}
|
||||
} catch {
|
||||
setFormError("An unexpected error occurred")
|
||||
} finally {
|
||||
setIsPending(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4 w-full max-w-sm">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">New Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Enter new password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">Confirm New Password</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
placeholder="Confirm new password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
{formError && (
|
||||
<p className="text-sm text-destructive">{formError}</p>
|
||||
)}
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{isPending ? "Resetting..." : "Reset password"}
|
||||
</Button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -1,29 +1,97 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { signIn, useSession } from "@/lib/auth-client";
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { signIn, useSession } from "@/lib/auth-client"
|
||||
|
||||
export function SignInButton() {
|
||||
const { data: session, isPending } = useSession();
|
||||
const { data: session, isPending: sessionPending } = useSession()
|
||||
const router = useRouter()
|
||||
const [email, setEmail] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
|
||||
if (isPending) {
|
||||
return <Button disabled>Loading...</Button>;
|
||||
if (sessionPending) {
|
||||
return <Button disabled>Loading...</Button>
|
||||
}
|
||||
|
||||
if (session) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError("")
|
||||
setIsPending(true)
|
||||
|
||||
try {
|
||||
const result = await signIn.email({
|
||||
email,
|
||||
password,
|
||||
callbackURL: "/dashboard",
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error.message || "Failed to sign in")
|
||||
} else {
|
||||
router.push("/dashboard")
|
||||
router.refresh()
|
||||
}
|
||||
} catch {
|
||||
setError("An unexpected error occurred")
|
||||
} finally {
|
||||
setIsPending(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await signIn.social({
|
||||
provider: "google",
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Sign in
|
||||
<form onSubmit={handleSubmit} className="space-y-4 w-full max-w-sm">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Your password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
)}
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{isPending ? "Signing in..." : "Sign in"}
|
||||
</Button>
|
||||
);
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
<Link href="/forgot-password" className="hover:underline">
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
Don't have an account?{" "}
|
||||
<Link href="/register" className="text-primary hover:underline">
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
121
create-agentic-app/template/src/components/auth/sign-up-form.tsx
Normal file
121
create-agentic-app/template/src/components/auth/sign-up-form.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { signUp } from "@/lib/auth-client"
|
||||
|
||||
export function SignUpForm() {
|
||||
const router = useRouter()
|
||||
const [name, setName] = useState("")
|
||||
const [email, setEmail] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [confirmPassword, setConfirmPassword] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError("")
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError("Passwords do not match")
|
||||
return
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
setError("Password must be at least 8 characters")
|
||||
return
|
||||
}
|
||||
|
||||
setIsPending(true)
|
||||
|
||||
try {
|
||||
const result = await signUp.email({
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
callbackURL: "/dashboard",
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error.message || "Failed to create account")
|
||||
} else {
|
||||
router.push("/dashboard")
|
||||
router.refresh()
|
||||
}
|
||||
} catch {
|
||||
setError("An unexpected error occurred")
|
||||
} finally {
|
||||
setIsPending(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4 w-full max-w-sm">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="Your name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Create a password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
placeholder="Confirm your password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
)}
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{isPending ? "Creating account..." : "Create account"}
|
||||
</Button>
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
Already have an account?{" "}
|
||||
<Link href="/login" className="text-primary hover:underline">
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { User, LogOut } from "lucide-react";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useSession, signOut } from "@/lib/auth-client";
|
||||
import { SignInButton } from "./sign-in-button";
|
||||
|
||||
export function UserProfile() {
|
||||
const { data: session, isPending } = useSession();
|
||||
@@ -25,8 +25,15 @@ export function UserProfile() {
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4 p-6">
|
||||
<SignInButton />
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="/login">
|
||||
<Button variant="ghost" size="sm">
|
||||
Sign in
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/register">
|
||||
<Button size="sm">Sign up</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,4 +10,7 @@ export const {
|
||||
signUp,
|
||||
useSession,
|
||||
getSession,
|
||||
requestPasswordReset,
|
||||
resetPassword,
|
||||
sendVerificationEmail,
|
||||
} = authClient
|
||||
@@ -6,10 +6,20 @@ export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg",
|
||||
}),
|
||||
socialProviders: {
|
||||
google: {
|
||||
clientId: process.env.GOOGLE_CLIENT_ID as string,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
sendResetPassword: async ({ user, url }) => {
|
||||
// Log password reset URL to terminal (no email integration yet)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`\n${"=".repeat(60)}\nPASSWORD RESET REQUEST\nUser: ${user.email}\nReset URL: ${url}\n${"=".repeat(60)}\n`)
|
||||
},
|
||||
},
|
||||
emailVerification: {
|
||||
sendOnSignUp: true,
|
||||
sendVerificationEmail: async ({ user, url }) => {
|
||||
// Log verification URL to terminal (no email integration yet)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`\n${"=".repeat(60)}\nEMAIL VERIFICATION\nUser: ${user.email}\nVerification URL: ${url}\n${"=".repeat(60)}\n`)
|
||||
},
|
||||
},
|
||||
})
|
||||
5
drizzle/0001_last_warpath.sql
Normal file
5
drizzle/0001_last_warpath.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
CREATE INDEX "account_user_id_idx" ON "account" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "account_provider_account_idx" ON "account" USING btree ("provider_id","account_id");--> statement-breakpoint
|
||||
CREATE INDEX "session_user_id_idx" ON "session" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "session_token_idx" ON "session" USING btree ("token");--> statement-breakpoint
|
||||
CREATE INDEX "user_email_idx" ON "user" USING btree ("email");
|
||||
410
drizzle/meta/0001_snapshot.json
Normal file
410
drizzle/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,410 @@
|
||||
{
|
||||
"id": "5737c145-8057-43dd-b149-155148e3dac7",
|
||||
"prevId": "56cf4573-0efe-4f7d-908f-0c7cb0ac0739",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"account_user_id_idx": {
|
||||
"name": "account_user_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"account_provider_account_idx": {
|
||||
"name": "account_provider_account_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "provider_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "account_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.session": {
|
||||
"name": "session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"session_user_id_idx": {
|
||||
"name": "session_user_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"session_token_idx": {
|
||||
"name": "session_token_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "token",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"session_token_unique": {
|
||||
"name": "session_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_email_idx": {
|
||||
"name": "user_email_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "email",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.verification": {
|
||||
"name": "verification",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,13 @@
|
||||
"when": 1762409965425,
|
||||
"tag": "0000_chilly_the_phantom",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1766719908715,
|
||||
"tag": "0001_last_warpath",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5,11 +5,6 @@ POSTGRES_URL=postgresql://dev_user:dev_password@localhost:5432/postgres_dev
|
||||
# Generate key using https://www.better-auth.com/docs/installation
|
||||
BETTER_AUTH_SECRET=qtD4Ssa0t5jY7ewALgai97sKhAtn7Ysc
|
||||
|
||||
# Google OAuth (Get from Google Cloud Console)
|
||||
# Redirect URI: http://localhost:3000/api/auth/callback/google
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
# AI Integration via OpenRouter (Optional - for chat functionality)
|
||||
# Get your API key from: https://openrouter.ai/settings/keys
|
||||
# View available models at: https://openrouter.ai/models
|
||||
|
||||
35
src/app/(auth)/forgot-password/page.tsx
Normal file
35
src/app/(auth)/forgot-password/page.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { headers } from "next/headers"
|
||||
import { redirect } from "next/navigation"
|
||||
import { ForgotPasswordForm } from "@/components/auth/forgot-password-form"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { auth } from "@/lib/auth"
|
||||
|
||||
export default async function ForgotPasswordPage() {
|
||||
const session = await auth.api.getSession({ headers: await headers() })
|
||||
|
||||
if (session) {
|
||||
redirect("/dashboard")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[calc(100vh-4rem)] items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle>Forgot password</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email address and we'll send you a reset link
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col items-center">
|
||||
<ForgotPasswordForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
7
src/app/(auth)/layout.tsx
Normal file
7
src/app/(auth)/layout.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return <>{children}</>
|
||||
}
|
||||
44
src/app/(auth)/login/page.tsx
Normal file
44
src/app/(auth)/login/page.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { headers } from "next/headers"
|
||||
import { redirect } from "next/navigation"
|
||||
import { SignInButton } from "@/components/auth/sign-in-button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { auth } from "@/lib/auth"
|
||||
|
||||
export default async function LoginPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ reset?: string }>
|
||||
}) {
|
||||
const session = await auth.api.getSession({ headers: await headers() })
|
||||
|
||||
if (session) {
|
||||
redirect("/dashboard")
|
||||
}
|
||||
|
||||
const { reset } = await searchParams
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[calc(100vh-4rem)] items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle>Welcome back</CardTitle>
|
||||
<CardDescription>Sign in to your account</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col items-center">
|
||||
{reset === "success" && (
|
||||
<p className="mb-4 text-sm text-green-600 dark:text-green-400">
|
||||
Password reset successfully. Please sign in with your new password.
|
||||
</p>
|
||||
)}
|
||||
<SignInButton />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
33
src/app/(auth)/register/page.tsx
Normal file
33
src/app/(auth)/register/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { headers } from "next/headers"
|
||||
import { redirect } from "next/navigation"
|
||||
import { SignUpForm } from "@/components/auth/sign-up-form"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { auth } from "@/lib/auth"
|
||||
|
||||
export default async function RegisterPage() {
|
||||
const session = await auth.api.getSession({ headers: await headers() })
|
||||
|
||||
if (session) {
|
||||
redirect("/dashboard")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[calc(100vh-4rem)] items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle>Create an account</CardTitle>
|
||||
<CardDescription>Get started with your new account</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col items-center">
|
||||
<SignUpForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
36
src/app/(auth)/reset-password/page.tsx
Normal file
36
src/app/(auth)/reset-password/page.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Suspense } from "react"
|
||||
import { headers } from "next/headers"
|
||||
import { redirect } from "next/navigation"
|
||||
import { ResetPasswordForm } from "@/components/auth/reset-password-form"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { auth } from "@/lib/auth"
|
||||
|
||||
export default async function ResetPasswordPage() {
|
||||
const session = await auth.api.getSession({ headers: await headers() })
|
||||
|
||||
if (session) {
|
||||
redirect("/dashboard")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[calc(100vh-4rem)] items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle>Reset password</CardTitle>
|
||||
<CardDescription>Enter your new password below</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col items-center">
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<ResetPasswordForm />
|
||||
</Suspense>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
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";
|
||||
@@ -33,7 +33,13 @@ export default function ProfilePage() {
|
||||
const [securityOpen, setSecurityOpen] = useState(false);
|
||||
const [emailPrefsOpen, setEmailPrefsOpen] = useState(false);
|
||||
|
||||
if (isPending) {
|
||||
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>
|
||||
@@ -41,11 +47,6 @@ export default function ProfilePage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
router.push("/");
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = session.user;
|
||||
const createdDate = user.createdAt
|
||||
? new Date(user.createdAt).toLocaleDateString("en-US", {
|
||||
|
||||
83
src/components/auth/forgot-password-form.tsx
Normal file
83
src/components/auth/forgot-password-form.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { requestPasswordReset } from "@/lib/auth-client"
|
||||
|
||||
export function ForgotPasswordForm() {
|
||||
const [email, setEmail] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
const [success, setSuccess] = useState(false)
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError("")
|
||||
setIsPending(true)
|
||||
|
||||
try {
|
||||
const result = await requestPasswordReset({
|
||||
email,
|
||||
redirectTo: "/reset-password",
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error.message || "Failed to send reset email")
|
||||
} else {
|
||||
setSuccess(true)
|
||||
}
|
||||
} catch {
|
||||
setError("An unexpected error occurred")
|
||||
} finally {
|
||||
setIsPending(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div className="space-y-4 w-full max-w-sm text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
If an account exists with that email, a password reset link has been sent.
|
||||
Check your terminal for the reset URL.
|
||||
</p>
|
||||
<Link href="/login">
|
||||
<Button variant="outline" className="w-full">
|
||||
Back to sign in
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4 w-full max-w-sm">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
)}
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{isPending ? "Sending..." : "Send reset link"}
|
||||
</Button>
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
Remember your password?{" "}
|
||||
<Link href="/login" className="text-primary hover:underline">
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
107
src/components/auth/reset-password-form.tsx
Normal file
107
src/components/auth/reset-password-form.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { resetPassword } from "@/lib/auth-client"
|
||||
|
||||
export function ResetPasswordForm() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const token = searchParams.get("token")
|
||||
const error = searchParams.get("error")
|
||||
|
||||
const [password, setPassword] = useState("")
|
||||
const [confirmPassword, setConfirmPassword] = useState("")
|
||||
const [formError, setFormError] = useState("")
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
|
||||
if (error === "invalid_token" || !token) {
|
||||
return (
|
||||
<div className="space-y-4 w-full max-w-sm text-center">
|
||||
<p className="text-sm text-destructive">
|
||||
{error === "invalid_token"
|
||||
? "This password reset link is invalid or has expired."
|
||||
: "No reset token provided."}
|
||||
</p>
|
||||
<Link href="/forgot-password">
|
||||
<Button variant="outline" className="w-full">
|
||||
Request a new link
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setFormError("")
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setFormError("Passwords do not match")
|
||||
return
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
setFormError("Password must be at least 8 characters")
|
||||
return
|
||||
}
|
||||
|
||||
setIsPending(true)
|
||||
|
||||
try {
|
||||
const result = await resetPassword({
|
||||
newPassword: password,
|
||||
token,
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
setFormError(result.error.message || "Failed to reset password")
|
||||
} else {
|
||||
router.push("/login?reset=success")
|
||||
}
|
||||
} catch {
|
||||
setFormError("An unexpected error occurred")
|
||||
} finally {
|
||||
setIsPending(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4 w-full max-w-sm">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">New Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Enter new password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">Confirm New Password</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
placeholder="Confirm new password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
{formError && (
|
||||
<p className="text-sm text-destructive">{formError}</p>
|
||||
)}
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{isPending ? "Resetting..." : "Reset password"}
|
||||
</Button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -1,29 +1,97 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { signIn, useSession } from "@/lib/auth-client";
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { signIn, useSession } from "@/lib/auth-client"
|
||||
|
||||
export function SignInButton() {
|
||||
const { data: session, isPending } = useSession();
|
||||
const { data: session, isPending: sessionPending } = useSession()
|
||||
const router = useRouter()
|
||||
const [email, setEmail] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
|
||||
if (isPending) {
|
||||
return <Button disabled>Loading...</Button>;
|
||||
if (sessionPending) {
|
||||
return <Button disabled>Loading...</Button>
|
||||
}
|
||||
|
||||
if (session) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError("")
|
||||
setIsPending(true)
|
||||
|
||||
try {
|
||||
const result = await signIn.email({
|
||||
email,
|
||||
password,
|
||||
callbackURL: "/dashboard",
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error.message || "Failed to sign in")
|
||||
} else {
|
||||
router.push("/dashboard")
|
||||
router.refresh()
|
||||
}
|
||||
} catch {
|
||||
setError("An unexpected error occurred")
|
||||
} finally {
|
||||
setIsPending(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await signIn.social({
|
||||
provider: "google",
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Sign in
|
||||
<form onSubmit={handleSubmit} className="space-y-4 w-full max-w-sm">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Your password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
)}
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{isPending ? "Signing in..." : "Sign in"}
|
||||
</Button>
|
||||
);
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
<Link href="/forgot-password" className="hover:underline">
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
Don't have an account?{" "}
|
||||
<Link href="/register" className="text-primary hover:underline">
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
121
src/components/auth/sign-up-form.tsx
Normal file
121
src/components/auth/sign-up-form.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { signUp } from "@/lib/auth-client"
|
||||
|
||||
export function SignUpForm() {
|
||||
const router = useRouter()
|
||||
const [name, setName] = useState("")
|
||||
const [email, setEmail] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [confirmPassword, setConfirmPassword] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError("")
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError("Passwords do not match")
|
||||
return
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
setError("Password must be at least 8 characters")
|
||||
return
|
||||
}
|
||||
|
||||
setIsPending(true)
|
||||
|
||||
try {
|
||||
const result = await signUp.email({
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
callbackURL: "/dashboard",
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error.message || "Failed to create account")
|
||||
} else {
|
||||
router.push("/dashboard")
|
||||
router.refresh()
|
||||
}
|
||||
} catch {
|
||||
setError("An unexpected error occurred")
|
||||
} finally {
|
||||
setIsPending(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4 w-full max-w-sm">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="Your name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Create a password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
placeholder="Confirm your password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
)}
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{isPending ? "Creating account..." : "Create account"}
|
||||
</Button>
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
Already have an account?{" "}
|
||||
<Link href="/login" className="text-primary hover:underline">
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { User, LogOut } from "lucide-react";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useSession, signOut } from "@/lib/auth-client";
|
||||
import { SignInButton } from "./sign-in-button";
|
||||
|
||||
export function UserProfile() {
|
||||
const { data: session, isPending } = useSession();
|
||||
@@ -25,8 +25,15 @@ export function UserProfile() {
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4 p-6">
|
||||
<SignInButton />
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="/login">
|
||||
<Button variant="ghost" size="sm">
|
||||
Sign in
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/register">
|
||||
<Button size="sm">Sign up</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,4 +10,7 @@ export const {
|
||||
signUp,
|
||||
useSession,
|
||||
getSession,
|
||||
requestPasswordReset,
|
||||
resetPassword,
|
||||
sendVerificationEmail,
|
||||
} = authClient
|
||||
@@ -6,10 +6,20 @@ export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg",
|
||||
}),
|
||||
socialProviders: {
|
||||
google: {
|
||||
clientId: process.env.GOOGLE_CLIENT_ID as string,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
sendResetPassword: async ({ user, url }) => {
|
||||
// Log password reset URL to terminal (no email integration yet)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`\n${"=".repeat(60)}\nPASSWORD RESET REQUEST\nUser: ${user.email}\nReset URL: ${url}\n${"=".repeat(60)}\n`)
|
||||
},
|
||||
},
|
||||
emailVerification: {
|
||||
sendOnSignUp: true,
|
||||
sendVerificationEmail: async ({ user, url }) => {
|
||||
// Log verification URL to terminal (no email integration yet)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`\n${"=".repeat(60)}\nEMAIL VERIFICATION\nUser: ${user.email}\nVerification URL: ${url}\n${"=".repeat(60)}\n`)
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user