diff --git a/.claude/agents/file-explorer.md b/.claude/agents/file-explorer.md
new file mode 100644
index 0000000..f24e0ab
--- /dev/null
+++ b/.claude/agents/file-explorer.md
@@ -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\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\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\n\n\n\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\nSince the user wants to discover project structure, use the file-explorer agent to scan and catalog the components.\n\n\n\n\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\nSince the user needs to locate files related to a specific functionality, use the file-explorer agent to search for matching files.\n\n\n\n\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\nProactively using the file-explorer agent to understand project layout before making architectural decisions.\n\n
+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.
diff --git a/.claude/commands/create-feature.md b/.claude/commands/create-spec.md
similarity index 98%
rename from .claude/commands/create-feature.md
rename to .claude/commands/create-spec.md
index d3444b2..e04562a 100644
--- a/.claude/commands/create-feature.md
+++ b/.claude/commands/create-spec.md
@@ -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
diff --git a/CLAUDE.md b/CLAUDE.md
index 58a5a8f..ec6a14e 100644
--- a/CLAUDE.md
+++ b/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
diff --git a/create-agentic-app/package-lock.json b/create-agentic-app/package-lock.json
index eb23688..1e58857 100644
--- a/create-agentic-app/package-lock.json
+++ b/create-agentic-app/package-lock.json
@@ -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",
diff --git a/create-agentic-app/package.json b/create-agentic-app/package.json
index c927fdd..b959673 100644
--- a/create-agentic-app/package.json
+++ b/create-agentic-app/package.json
@@ -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": {
diff --git a/create-agentic-app/template/.claude/agents/file-explorer.md b/create-agentic-app/template/.claude/agents/file-explorer.md
new file mode 100644
index 0000000..f24e0ab
--- /dev/null
+++ b/create-agentic-app/template/.claude/agents/file-explorer.md
@@ -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\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\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\n\n\n\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\nSince the user wants to discover project structure, use the file-explorer agent to scan and catalog the components.\n\n\n\n\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\nSince the user needs to locate files related to a specific functionality, use the file-explorer agent to search for matching files.\n\n\n\n\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\nProactively using the file-explorer agent to understand project layout before making architectural decisions.\n\n
+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.
diff --git a/create-agentic-app/template/.claude/commands/create-feature.md b/create-agentic-app/template/.claude/commands/create-spec.md
similarity index 98%
rename from create-agentic-app/template/.claude/commands/create-feature.md
rename to create-agentic-app/template/.claude/commands/create-spec.md
index d3444b2..e04562a 100644
--- a/create-agentic-app/template/.claude/commands/create-feature.md
+++ b/create-agentic-app/template/.claude/commands/create-spec.md
@@ -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
diff --git a/create-agentic-app/template/.playwright-mcp/dark-mode-homepage.png b/create-agentic-app/template/.playwright-mcp/dark-mode-homepage.png
deleted file mode 100644
index a696de7..0000000
Binary files a/create-agentic-app/template/.playwright-mcp/dark-mode-homepage.png and /dev/null differ
diff --git a/create-agentic-app/template/.playwright-mcp/light-mode-homepage.png b/create-agentic-app/template/.playwright-mcp/light-mode-homepage.png
deleted file mode 100644
index 86f619f..0000000
Binary files a/create-agentic-app/template/.playwright-mcp/light-mode-homepage.png and /dev/null differ
diff --git a/create-agentic-app/template/.playwright-mcp/page-2025-11-07T05-24-10-069Z.png b/create-agentic-app/template/.playwright-mcp/page-2025-11-07T05-24-10-069Z.png
deleted file mode 100644
index 70f86c2..0000000
Binary files a/create-agentic-app/template/.playwright-mcp/page-2025-11-07T05-24-10-069Z.png and /dev/null differ
diff --git a/create-agentic-app/template/CLAUDE.md b/create-agentic-app/template/CLAUDE.md
index 58a5a8f..ec6a14e 100644
--- a/create-agentic-app/template/CLAUDE.md
+++ b/create-agentic-app/template/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
diff --git a/create-agentic-app/template/drizzle/0001_last_warpath.sql b/create-agentic-app/template/drizzle/0001_last_warpath.sql
new file mode 100644
index 0000000..494dd8a
--- /dev/null
+++ b/create-agentic-app/template/drizzle/0001_last_warpath.sql
@@ -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");
\ No newline at end of file
diff --git a/create-agentic-app/template/drizzle/meta/0001_snapshot.json b/create-agentic-app/template/drizzle/meta/0001_snapshot.json
new file mode 100644
index 0000000..9d6fc79
--- /dev/null
+++ b/create-agentic-app/template/drizzle/meta/0001_snapshot.json
@@ -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": {}
+ }
+}
\ No newline at end of file
diff --git a/create-agentic-app/template/drizzle/meta/_journal.json b/create-agentic-app/template/drizzle/meta/_journal.json
index 63d838e..81aebd8 100644
--- a/create-agentic-app/template/drizzle/meta/_journal.json
+++ b/create-agentic-app/template/drizzle/meta/_journal.json
@@ -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
}
]
}
\ No newline at end of file
diff --git a/create-agentic-app/template/env.example b/create-agentic-app/template/env.example
index 1854928..a0b471a 100644
--- a/create-agentic-app/template/env.example
+++ b/create-agentic-app/template/env.example
@@ -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
diff --git a/create-agentic-app/template/src/app/(auth)/forgot-password/page.tsx b/create-agentic-app/template/src/app/(auth)/forgot-password/page.tsx
new file mode 100644
index 0000000..3b95371
--- /dev/null
+++ b/create-agentic-app/template/src/app/(auth)/forgot-password/page.tsx
@@ -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 (
+
+
+
+ Forgot password
+
+ Enter your email address and we'll send you a reset link
+
+
+
+
+
+
+
+ )
+}
diff --git a/create-agentic-app/template/src/app/(auth)/layout.tsx b/create-agentic-app/template/src/app/(auth)/layout.tsx
new file mode 100644
index 0000000..48eacd2
--- /dev/null
+++ b/create-agentic-app/template/src/app/(auth)/layout.tsx
@@ -0,0 +1,7 @@
+export default function AuthLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return <>{children}>
+}
diff --git a/create-agentic-app/template/src/app/(auth)/login/page.tsx b/create-agentic-app/template/src/app/(auth)/login/page.tsx
new file mode 100644
index 0000000..003503b
--- /dev/null
+++ b/create-agentic-app/template/src/app/(auth)/login/page.tsx
@@ -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 (
+
+
+
+ Welcome back
+ Sign in to your account
+
+
+ {reset === "success" && (
+
+ Password reset successfully. Please sign in with your new password.
+
+ )}
+
+
+
+
+ )
+}
diff --git a/create-agentic-app/template/src/app/(auth)/register/page.tsx b/create-agentic-app/template/src/app/(auth)/register/page.tsx
new file mode 100644
index 0000000..449ecd7
--- /dev/null
+++ b/create-agentic-app/template/src/app/(auth)/register/page.tsx
@@ -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 (
+
+
+
+ Create an account
+ Get started with your new account
+
+
+
+
+
+
+ )
+}
diff --git a/create-agentic-app/template/src/app/(auth)/reset-password/page.tsx b/create-agentic-app/template/src/app/(auth)/reset-password/page.tsx
new file mode 100644
index 0000000..aa3b888
--- /dev/null
+++ b/create-agentic-app/template/src/app/(auth)/reset-password/page.tsx
@@ -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 (
+
+
+
+ Reset password
+ Enter your new password below
+
+
+ Loading...
}>
+
+
+
+
+
+ )
+}
diff --git a/create-agentic-app/template/src/app/profile/page.tsx b/create-agentic-app/template/src/app/profile/page.tsx
index 6a7870e..a33bbbb 100644
--- a/create-agentic-app/template/src/app/profile/page.tsx
+++ b/create-agentic-app/template/src/app/profile/page.tsx
@@ -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 (
Loading...
@@ -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", {
diff --git a/create-agentic-app/template/src/components/auth/forgot-password-form.tsx b/create-agentic-app/template/src/components/auth/forgot-password-form.tsx
new file mode 100644
index 0000000..b8f1dd8
--- /dev/null
+++ b/create-agentic-app/template/src/components/auth/forgot-password-form.tsx
@@ -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 (
+
+
+ If an account exists with that email, a password reset link has been sent.
+ Check your terminal for the reset URL.
+
+
+
+
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/create-agentic-app/template/src/components/auth/reset-password-form.tsx b/create-agentic-app/template/src/components/auth/reset-password-form.tsx
new file mode 100644
index 0000000..ff66d3a
--- /dev/null
+++ b/create-agentic-app/template/src/components/auth/reset-password-form.tsx
@@ -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 (
+
+
+ {error === "invalid_token"
+ ? "This password reset link is invalid or has expired."
+ : "No reset token provided."}
+
+
+
+
+
+ )
+ }
+
+ 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 (
+
+ )
+}
diff --git a/create-agentic-app/template/src/components/auth/sign-in-button.tsx b/create-agentic-app/template/src/components/auth/sign-in-button.tsx
index 7bed448..8899f0d 100644
--- a/create-agentic-app/template/src/components/auth/sign-in-button.tsx
+++ b/create-agentic-app/template/src/components/auth/sign-in-button.tsx
@@ -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
;
+ if (sessionPending) {
+ return
}
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 (
-
- );
+
+ )
}
diff --git a/create-agentic-app/template/src/components/auth/sign-up-form.tsx b/create-agentic-app/template/src/components/auth/sign-up-form.tsx
new file mode 100644
index 0000000..92038aa
--- /dev/null
+++ b/create-agentic-app/template/src/components/auth/sign-up-form.tsx
@@ -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 (
+
+ )
+}
diff --git a/create-agentic-app/template/src/components/auth/user-profile.tsx b/create-agentic-app/template/src/components/auth/user-profile.tsx
index 34f0ec5..09b44ee 100644
--- a/create-agentic-app/template/src/components/auth/user-profile.tsx
+++ b/create-agentic-app/template/src/components/auth/user-profile.tsx
@@ -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 (
-
-
+
+
+
+
+
+
+
);
}
diff --git a/create-agentic-app/template/src/lib/auth-client.ts b/create-agentic-app/template/src/lib/auth-client.ts
index 951c086..3afa911 100644
--- a/create-agentic-app/template/src/lib/auth-client.ts
+++ b/create-agentic-app/template/src/lib/auth-client.ts
@@ -10,4 +10,7 @@ export const {
signUp,
useSession,
getSession,
+ requestPasswordReset,
+ resetPassword,
+ sendVerificationEmail,
} = authClient
\ No newline at end of file
diff --git a/create-agentic-app/template/src/lib/auth.ts b/create-agentic-app/template/src/lib/auth.ts
index 731a7ed..1deb11f 100644
--- a/create-agentic-app/template/src/lib/auth.ts
+++ b/create-agentic-app/template/src/lib/auth.ts
@@ -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`)
},
},
})
\ No newline at end of file
diff --git a/drizzle/0001_last_warpath.sql b/drizzle/0001_last_warpath.sql
new file mode 100644
index 0000000..494dd8a
--- /dev/null
+++ b/drizzle/0001_last_warpath.sql
@@ -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");
\ No newline at end of file
diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json
new file mode 100644
index 0000000..9d6fc79
--- /dev/null
+++ b/drizzle/meta/0001_snapshot.json
@@ -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": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index 63d838e..81aebd8 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -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
}
]
}
\ No newline at end of file
diff --git a/env.example b/env.example
index 1854928..a0b471a 100644
--- a/env.example
+++ b/env.example
@@ -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
diff --git a/src/app/(auth)/forgot-password/page.tsx b/src/app/(auth)/forgot-password/page.tsx
new file mode 100644
index 0000000..3b95371
--- /dev/null
+++ b/src/app/(auth)/forgot-password/page.tsx
@@ -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 (
+
+
+
+ Forgot password
+
+ Enter your email address and we'll send you a reset link
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx
new file mode 100644
index 0000000..48eacd2
--- /dev/null
+++ b/src/app/(auth)/layout.tsx
@@ -0,0 +1,7 @@
+export default function AuthLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return <>{children}>
+}
diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx
new file mode 100644
index 0000000..003503b
--- /dev/null
+++ b/src/app/(auth)/login/page.tsx
@@ -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 (
+
+
+
+ Welcome back
+ Sign in to your account
+
+
+ {reset === "success" && (
+
+ Password reset successfully. Please sign in with your new password.
+
+ )}
+
+
+
+
+ )
+}
diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx
new file mode 100644
index 0000000..449ecd7
--- /dev/null
+++ b/src/app/(auth)/register/page.tsx
@@ -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 (
+
+
+
+ Create an account
+ Get started with your new account
+
+
+
+
+
+
+ )
+}
diff --git a/src/app/(auth)/reset-password/page.tsx b/src/app/(auth)/reset-password/page.tsx
new file mode 100644
index 0000000..aa3b888
--- /dev/null
+++ b/src/app/(auth)/reset-password/page.tsx
@@ -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 (
+
+
+
+ Reset password
+ Enter your new password below
+
+
+ Loading...
}>
+
+
+
+
+
+ )
+}
diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx
index 6a7870e..a33bbbb 100644
--- a/src/app/profile/page.tsx
+++ b/src/app/profile/page.tsx
@@ -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 (
Loading...
@@ -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", {
diff --git a/src/components/auth/forgot-password-form.tsx b/src/components/auth/forgot-password-form.tsx
new file mode 100644
index 0000000..b8f1dd8
--- /dev/null
+++ b/src/components/auth/forgot-password-form.tsx
@@ -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 (
+
+
+ If an account exists with that email, a password reset link has been sent.
+ Check your terminal for the reset URL.
+
+
+
+
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/src/components/auth/reset-password-form.tsx b/src/components/auth/reset-password-form.tsx
new file mode 100644
index 0000000..ff66d3a
--- /dev/null
+++ b/src/components/auth/reset-password-form.tsx
@@ -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 (
+
+
+ {error === "invalid_token"
+ ? "This password reset link is invalid or has expired."
+ : "No reset token provided."}
+
+
+
+
+
+ )
+ }
+
+ 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 (
+
+ )
+}
diff --git a/src/components/auth/sign-in-button.tsx b/src/components/auth/sign-in-button.tsx
index 7bed448..8899f0d 100644
--- a/src/components/auth/sign-in-button.tsx
+++ b/src/components/auth/sign-in-button.tsx
@@ -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
;
+ if (sessionPending) {
+ return
}
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 (
-
- );
+
+ )
}
diff --git a/src/components/auth/sign-up-form.tsx b/src/components/auth/sign-up-form.tsx
new file mode 100644
index 0000000..92038aa
--- /dev/null
+++ b/src/components/auth/sign-up-form.tsx
@@ -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 (
+
+ )
+}
diff --git a/src/components/auth/user-profile.tsx b/src/components/auth/user-profile.tsx
index 34f0ec5..09b44ee 100644
--- a/src/components/auth/user-profile.tsx
+++ b/src/components/auth/user-profile.tsx
@@ -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 (
-
-
+
+
+
+
+
+
+
);
}
diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts
index 951c086..3afa911 100644
--- a/src/lib/auth-client.ts
+++ b/src/lib/auth-client.ts
@@ -10,4 +10,7 @@ export const {
signUp,
useSession,
getSession,
+ requestPasswordReset,
+ resetPassword,
+ sendVerificationEmail,
} = authClient
\ No newline at end of file
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index 731a7ed..1deb11f 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -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`)
},
},
})
\ No newline at end of file