@vishwanathb/vishwa-auth 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/bin/vishwa-auth.js +102 -0
- package/package.json +61 -0
- package/scripts/setup.js +105 -0
- package/templates/.env.example +50 -0
- package/templates/app/(auth)/login/page.tsx +57 -0
- package/templates/app/(auth)/register/page.tsx +61 -0
- package/templates/app/api/auth/[...all]/route.ts +4 -0
- package/templates/app/dashboard/page.tsx +32 -0
- package/templates/lib/auth-client.ts +3 -0
- package/templates/lib/auth.ts +16 -0
- package/templates/lib/prisma.ts +22 -0
- package/templates/middleware.ts +27 -0
- package/templates/prisma/schema.prisma +75 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vishwanath B
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# vishwa-auth
|
|
2
|
+
|
|
3
|
+
**Better-auth scaffold for Next.js** - `npm i vishwa-auth` and get complete auth setup.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i vishwa-auth better-auth @prisma/client prisma
|
|
9
|
+
cp .env.example .env.local
|
|
10
|
+
npx prisma generate && npx prisma db push
|
|
11
|
+
npm run dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
See [SETUP.md](./SETUP.md) for detailed instructions.
|
|
15
|
+
|
|
16
|
+
## What's Installed
|
|
17
|
+
|
|
18
|
+
| File | Purpose |
|
|
19
|
+
|------|---------|
|
|
20
|
+
| `lib/auth.ts` | better-auth server config |
|
|
21
|
+
| `lib/auth-client.ts` | React client hooks |
|
|
22
|
+
| `app/api/auth/[...all]/route.ts` | Auth API handler |
|
|
23
|
+
| `app/(auth)/*` | Login, Register pages |
|
|
24
|
+
| `app/dashboard/*` | Protected page example |
|
|
25
|
+
| `middleware.ts` | Route protection |
|
|
26
|
+
| `prisma/schema.prisma` | Database models |
|
|
27
|
+
|
|
28
|
+
## Skip Postinstall
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
VISHWA_AUTH_SKIP_POSTINSTALL=true npm i vishwa-auth
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## License
|
|
35
|
+
|
|
36
|
+
MIT
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* vishwa-auth CLI
|
|
5
|
+
*
|
|
6
|
+
* Commands:
|
|
7
|
+
* init - Initialize auth files in current project
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
import fs from 'fs-extra';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
18
|
+
|
|
19
|
+
const program = new Command();
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.name('vishwa-auth')
|
|
23
|
+
.description('Production-ready authentication scaffold for Next.js')
|
|
24
|
+
.version('0.1.0');
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.command('init')
|
|
28
|
+
.description('Initialize vishwa-auth in current project')
|
|
29
|
+
.option('-f, --force', 'Overwrite existing files without backup')
|
|
30
|
+
.option('--dry-run', 'Show what would be copied without making changes')
|
|
31
|
+
.option('-v, --verbose', 'Show detailed output')
|
|
32
|
+
.action(async (options) => {
|
|
33
|
+
console.log('\n' + chalk.bold.magenta('🔐 vishwa-auth - Initializing authentication...'));
|
|
34
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
35
|
+
|
|
36
|
+
const templatesDir = path.resolve(__dirname, '..', 'templates');
|
|
37
|
+
const hostDir = process.cwd();
|
|
38
|
+
|
|
39
|
+
if (!await fs.pathExists(templatesDir)) {
|
|
40
|
+
console.log(chalk.red('Error: Templates directory not found.'));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = { created: 0, backed: 0, errors: 0 };
|
|
45
|
+
|
|
46
|
+
async function copyFile(src, dest) {
|
|
47
|
+
try {
|
|
48
|
+
const exists = await fs.pathExists(dest);
|
|
49
|
+
if (exists && !options.force) {
|
|
50
|
+
const backupPath = `${dest}.vishwa-auth.bak`;
|
|
51
|
+
if (!options.dryRun) {
|
|
52
|
+
await fs.copy(dest, backupPath);
|
|
53
|
+
await fs.copy(src, dest);
|
|
54
|
+
}
|
|
55
|
+
result.backed++;
|
|
56
|
+
if (options.verbose) console.log(chalk.yellow(` ⚠ ${path.relative(hostDir, dest)} (backed up)`));
|
|
57
|
+
} else {
|
|
58
|
+
if (!options.dryRun) {
|
|
59
|
+
await fs.ensureDir(path.dirname(dest));
|
|
60
|
+
await fs.copy(src, dest);
|
|
61
|
+
}
|
|
62
|
+
result.created++;
|
|
63
|
+
if (options.verbose) console.log(chalk.green(` ✓ ${path.relative(hostDir, dest)}`));
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
result.errors++;
|
|
67
|
+
if (options.verbose) console.log(chalk.red(` ✗ ${path.relative(hostDir, dest)}: ${error}`));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function copyDir(src, dest) {
|
|
72
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
const srcPath = path.join(src, entry.name);
|
|
75
|
+
const destPath = path.join(dest, entry.name);
|
|
76
|
+
if (entry.isDirectory()) {
|
|
77
|
+
await copyDir(srcPath, destPath);
|
|
78
|
+
} else {
|
|
79
|
+
await copyFile(srcPath, destPath);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (options.dryRun) console.log(chalk.cyan('Dry run mode - no files will be modified\n'));
|
|
85
|
+
|
|
86
|
+
await copyDir(templatesDir, hostDir);
|
|
87
|
+
|
|
88
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
89
|
+
console.log(chalk.green(`✓ Created: ${result.created} files`));
|
|
90
|
+
if (result.backed > 0) console.log(chalk.yellow(`⚠ Backed up: ${result.backed} files`));
|
|
91
|
+
if (result.errors > 0) console.log(chalk.red(`✗ Errors: ${result.errors}`));
|
|
92
|
+
|
|
93
|
+
if (!options.dryRun) {
|
|
94
|
+
console.log(chalk.bold('\n📝 Next steps:'));
|
|
95
|
+
console.log(' 1. cp .env.example .env.local');
|
|
96
|
+
console.log(' 2. npx prisma generate');
|
|
97
|
+
console.log(' 3. npx prisma migrate dev');
|
|
98
|
+
console.log(' 4. npm run dev');
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vishwanathb/vishwa-auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Production-ready better-auth scaffold for Next.js (App Router, TypeScript). Auto-installs complete auth setup.",
|
|
5
|
+
"author": "Vishwanath B",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"files": [
|
|
9
|
+
"templates",
|
|
10
|
+
"bin",
|
|
11
|
+
"scripts"
|
|
12
|
+
],
|
|
13
|
+
"bin": {
|
|
14
|
+
"vishwa-auth": "./bin/vishwa-auth.js"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"postinstall": "node scripts/setup.js",
|
|
18
|
+
"format": "prettier --write ."
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"chalk": "^5.4.1",
|
|
22
|
+
"commander": "^13.1.0",
|
|
23
|
+
"fs-extra": "^11.3.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/fs-extra": "^11.0.4",
|
|
27
|
+
"@types/node": "^22.13.4",
|
|
28
|
+
"prettier": "^3.5.1",
|
|
29
|
+
"typescript": "^5.7.3"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"better-auth": ">=1.0.0",
|
|
33
|
+
"@prisma/client": ">=5.0.0",
|
|
34
|
+
"next": ">=14.0.0",
|
|
35
|
+
"react": ">=18.0.0",
|
|
36
|
+
"react-dom": ">=18.0.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"@prisma/client": {
|
|
40
|
+
"optional": true
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=20.0.0"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"nextjs",
|
|
48
|
+
"auth",
|
|
49
|
+
"authentication",
|
|
50
|
+
"better-auth",
|
|
51
|
+
"prisma",
|
|
52
|
+
"scaffold",
|
|
53
|
+
"typescript",
|
|
54
|
+
"app-router"
|
|
55
|
+
],
|
|
56
|
+
"repository": {
|
|
57
|
+
"type": "git",
|
|
58
|
+
"url": "https://github.com/vishwanathb/vishwa-auth.git"
|
|
59
|
+
},
|
|
60
|
+
"homepage": "https://github.com/vishwanathb/vishwa-auth#readme"
|
|
61
|
+
}
|
package/scripts/setup.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* vishwa-auth - Postinstall Setup Script
|
|
5
|
+
*
|
|
6
|
+
* Runs automatically after npm install to scaffold auth files into host project.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs-extra';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
// Skip if env variable is set
|
|
17
|
+
if (process.env.VISHWA_AUTH_SKIP_POSTINSTALL === 'true') {
|
|
18
|
+
console.log('vishwa-auth: Skipping postinstall');
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Find the host project root (go up from node_modules/vishwa-auth/scripts)
|
|
23
|
+
function findHostRoot() {
|
|
24
|
+
let current = __dirname;
|
|
25
|
+
for (let i = 0; i < 10; i++) {
|
|
26
|
+
const parent = path.dirname(current);
|
|
27
|
+
const parentName = path.basename(parent);
|
|
28
|
+
if (parentName === 'node_modules') {
|
|
29
|
+
// Found node_modules, host root is one level up
|
|
30
|
+
return path.dirname(parent);
|
|
31
|
+
}
|
|
32
|
+
current = parent;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const hostDir = findHostRoot();
|
|
38
|
+
|
|
39
|
+
// Skip if we can't find the host or if it's the package itself
|
|
40
|
+
if (!hostDir) {
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const hostPackageJson = path.join(hostDir, 'package.json');
|
|
45
|
+
if (!fs.existsSync(hostPackageJson)) {
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const pkg = JSON.parse(fs.readFileSync(hostPackageJson, 'utf-8'));
|
|
51
|
+
if (pkg.name === 'vishwa-auth') {
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function setup() {
|
|
59
|
+
console.log('\n🔐 vishwa-auth - Setting up authentication...\n');
|
|
60
|
+
|
|
61
|
+
const templatesDir = path.resolve(__dirname, '..', 'templates');
|
|
62
|
+
|
|
63
|
+
if (!fs.existsSync(templatesDir)) {
|
|
64
|
+
console.log('Templates not found. Run: npx vishwa-auth init');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let created = 0, backed = 0;
|
|
69
|
+
|
|
70
|
+
async function copyFile(src, dest) {
|
|
71
|
+
const exists = await fs.pathExists(dest);
|
|
72
|
+
if (exists) {
|
|
73
|
+
await fs.copy(dest, `${dest}.vishwa-auth.bak`);
|
|
74
|
+
backed++;
|
|
75
|
+
}
|
|
76
|
+
await fs.ensureDir(path.dirname(dest));
|
|
77
|
+
await fs.copy(src, dest);
|
|
78
|
+
created++;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function copyDir(src, dest) {
|
|
82
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
const srcPath = path.join(src, entry.name);
|
|
85
|
+
const destPath = path.join(dest, entry.name);
|
|
86
|
+
if (entry.isDirectory()) {
|
|
87
|
+
await copyDir(srcPath, destPath);
|
|
88
|
+
} else {
|
|
89
|
+
await copyFile(srcPath, destPath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await copyDir(templatesDir, hostDir);
|
|
95
|
+
|
|
96
|
+
console.log(`✓ Created ${created} files`);
|
|
97
|
+
if (backed > 0) console.log(`⚠ Backed up ${backed} existing files`);
|
|
98
|
+
console.log('\n📝 Next steps:');
|
|
99
|
+
console.log(' 1. npm install better-auth @prisma/client prisma');
|
|
100
|
+
console.log(' 2. Copy .env.example to .env.local');
|
|
101
|
+
console.log(' 3. npx prisma generate && npx prisma db push');
|
|
102
|
+
console.log(' 4. npm run dev\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setup().catch(console.error);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# ==============================================
|
|
2
|
+
# vishwa-auth Environment Variables (better-auth)
|
|
3
|
+
# ==============================================
|
|
4
|
+
# Copy this file to .env.local and fill in your values.
|
|
5
|
+
# NEVER commit .env.local to version control!
|
|
6
|
+
|
|
7
|
+
# ==============================================
|
|
8
|
+
# Database
|
|
9
|
+
# ==============================================
|
|
10
|
+
# PostgreSQL (production)
|
|
11
|
+
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"
|
|
12
|
+
|
|
13
|
+
# SQLite (local development)
|
|
14
|
+
# DATABASE_URL="file:./dev.db"
|
|
15
|
+
|
|
16
|
+
# ==============================================
|
|
17
|
+
# Better Auth Configuration
|
|
18
|
+
# ==============================================
|
|
19
|
+
# REQUIRED: Generate a secure random string (at least 32 characters)
|
|
20
|
+
# Run: openssl rand -base64 32
|
|
21
|
+
BETTER_AUTH_SECRET="your-super-secret-key-change-this-in-production"
|
|
22
|
+
|
|
23
|
+
# Base URL of your application
|
|
24
|
+
BETTER_AUTH_URL="http://localhost:3000"
|
|
25
|
+
|
|
26
|
+
# ==============================================
|
|
27
|
+
# Email (SMTP) - Optional
|
|
28
|
+
# ==============================================
|
|
29
|
+
# For email verification and password reset
|
|
30
|
+
SMTP_HOST="smtp.example.com"
|
|
31
|
+
SMTP_PORT="587"
|
|
32
|
+
SMTP_USER="your-email@example.com"
|
|
33
|
+
SMTP_PASSWORD="your-email-password"
|
|
34
|
+
SMTP_FROM="noreply@yourapp.com"
|
|
35
|
+
|
|
36
|
+
# ==============================================
|
|
37
|
+
# OAuth Providers (Optional)
|
|
38
|
+
# ==============================================
|
|
39
|
+
# Google OAuth
|
|
40
|
+
# GOOGLE_CLIENT_ID=""
|
|
41
|
+
# GOOGLE_CLIENT_SECRET=""
|
|
42
|
+
|
|
43
|
+
# GitHub OAuth
|
|
44
|
+
# GITHUB_CLIENT_ID=""
|
|
45
|
+
# GITHUB_CLIENT_SECRET=""
|
|
46
|
+
|
|
47
|
+
# ==============================================
|
|
48
|
+
# Application URLs
|
|
49
|
+
# ==============================================
|
|
50
|
+
NEXT_PUBLIC_APP_URL="http://localhost:3000"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import Link from 'next/link';
|
|
6
|
+
import { authClient } from '@/lib/auth-client';
|
|
7
|
+
|
|
8
|
+
export default function LoginPage() {
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
const [email, setEmail] = useState('');
|
|
11
|
+
const [password, setPassword] = useState('');
|
|
12
|
+
const [error, setError] = useState('');
|
|
13
|
+
const [loading, setLoading] = useState(false);
|
|
14
|
+
|
|
15
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
setLoading(true);
|
|
18
|
+
setError('');
|
|
19
|
+
|
|
20
|
+
const { error } = await authClient.signIn.email({ email, password });
|
|
21
|
+
|
|
22
|
+
if (error) {
|
|
23
|
+
setError(error.message);
|
|
24
|
+
setLoading(false);
|
|
25
|
+
} else {
|
|
26
|
+
router.push('/dashboard');
|
|
27
|
+
router.refresh();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="min-h-screen flex items-center justify-center bg-gray-900">
|
|
33
|
+
<div className="w-full max-w-md bg-gray-800 rounded-lg p-8">
|
|
34
|
+
<h1 className="text-2xl font-bold text-white mb-6 text-center">Sign In</h1>
|
|
35
|
+
|
|
36
|
+
{error && <p className="text-red-400 mb-4 text-sm">{error}</p>}
|
|
37
|
+
|
|
38
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
39
|
+
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required
|
|
40
|
+
className="w-full px-4 py-3 rounded bg-gray-700 text-white border border-gray-600"
|
|
41
|
+
placeholder="Email" />
|
|
42
|
+
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required
|
|
43
|
+
className="w-full px-4 py-3 rounded bg-gray-700 text-white border border-gray-600"
|
|
44
|
+
placeholder="Password" />
|
|
45
|
+
<button type="submit" disabled={loading}
|
|
46
|
+
className="w-full py-3 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50">
|
|
47
|
+
{loading ? 'Signing in...' : 'Sign In'}
|
|
48
|
+
</button>
|
|
49
|
+
</form>
|
|
50
|
+
|
|
51
|
+
<p className="mt-4 text-center text-gray-400">
|
|
52
|
+
No account? <Link href="/register" className="text-blue-400">Sign up</Link>
|
|
53
|
+
</p>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import Link from 'next/link';
|
|
6
|
+
import { authClient } from '@/lib/auth-client';
|
|
7
|
+
|
|
8
|
+
export default function RegisterPage() {
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
const [name, setName] = useState('');
|
|
11
|
+
const [email, setEmail] = useState('');
|
|
12
|
+
const [password, setPassword] = useState('');
|
|
13
|
+
const [error, setError] = useState('');
|
|
14
|
+
const [loading, setLoading] = useState(false);
|
|
15
|
+
|
|
16
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
setLoading(true);
|
|
19
|
+
setError('');
|
|
20
|
+
|
|
21
|
+
const { error } = await authClient.signUp.email({ email, password, name });
|
|
22
|
+
|
|
23
|
+
if (error) {
|
|
24
|
+
setError(error.message);
|
|
25
|
+
setLoading(false);
|
|
26
|
+
} else {
|
|
27
|
+
router.push('/dashboard');
|
|
28
|
+
router.refresh();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="min-h-screen flex items-center justify-center bg-gray-900">
|
|
34
|
+
<div className="w-full max-w-md bg-gray-800 rounded-lg p-8">
|
|
35
|
+
<h1 className="text-2xl font-bold text-white mb-6 text-center">Create Account</h1>
|
|
36
|
+
|
|
37
|
+
{error && <p className="text-red-400 mb-4 text-sm">{error}</p>}
|
|
38
|
+
|
|
39
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
40
|
+
<input type="text" value={name} onChange={(e) => setName(e.target.value)}
|
|
41
|
+
className="w-full px-4 py-3 rounded bg-gray-700 text-white border border-gray-600"
|
|
42
|
+
placeholder="Name" />
|
|
43
|
+
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required
|
|
44
|
+
className="w-full px-4 py-3 rounded bg-gray-700 text-white border border-gray-600"
|
|
45
|
+
placeholder="Email" />
|
|
46
|
+
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required
|
|
47
|
+
className="w-full px-4 py-3 rounded bg-gray-700 text-white border border-gray-600"
|
|
48
|
+
placeholder="Password (min 8 chars)" />
|
|
49
|
+
<button type="submit" disabled={loading}
|
|
50
|
+
className="w-full py-3 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50">
|
|
51
|
+
{loading ? 'Creating...' : 'Create Account'}
|
|
52
|
+
</button>
|
|
53
|
+
</form>
|
|
54
|
+
|
|
55
|
+
<p className="mt-4 text-center text-gray-400">
|
|
56
|
+
Have an account? <Link href="/login" className="text-blue-400">Sign in</Link>
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useRouter } from 'next/navigation';
|
|
4
|
+
import { authClient } from '@/lib/auth-client';
|
|
5
|
+
|
|
6
|
+
export default function DashboardPage() {
|
|
7
|
+
const { data: session, isPending } = authClient.useSession();
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
|
|
10
|
+
if (isPending) return <div className="min-h-screen flex items-center justify-center bg-gray-900 text-white">Loading...</div>;
|
|
11
|
+
|
|
12
|
+
const handleSignOut = async () => {
|
|
13
|
+
await authClient.signOut();
|
|
14
|
+
router.push('/login');
|
|
15
|
+
router.refresh();
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="min-h-screen bg-gray-900">
|
|
20
|
+
<nav className="bg-gray-800 p-4 flex justify-between items-center">
|
|
21
|
+
<h1 className="text-xl font-bold text-white">Dashboard</h1>
|
|
22
|
+
<button onClick={handleSignOut} className="text-gray-400 hover:text-white">Sign out</button>
|
|
23
|
+
</nav>
|
|
24
|
+
<main className="p-8">
|
|
25
|
+
<div className="bg-gray-800 rounded-lg p-6">
|
|
26
|
+
<h2 className="text-xl font-bold text-white mb-4">Welcome!</h2>
|
|
27
|
+
<p className="text-gray-400">Signed in as: <span className="text-blue-400">{session?.user?.email}</span></p>
|
|
28
|
+
</div>
|
|
29
|
+
</main>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Better Auth Server Configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { betterAuth } from 'better-auth';
|
|
6
|
+
import { prismaAdapter } from 'better-auth/adapters/prisma';
|
|
7
|
+
import { prisma } from './prisma';
|
|
8
|
+
|
|
9
|
+
export const auth = betterAuth({
|
|
10
|
+
database: prismaAdapter(prisma, {
|
|
11
|
+
provider: 'postgresql', // Change to 'sqlite' for local dev
|
|
12
|
+
}),
|
|
13
|
+
emailAndPassword: {
|
|
14
|
+
enabled: true,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prisma Client Singleton
|
|
3
|
+
*
|
|
4
|
+
* This file provides a singleton instance of Prisma Client
|
|
5
|
+
* to prevent multiple instances in development.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { PrismaClient } from '@prisma/client';
|
|
9
|
+
|
|
10
|
+
const globalForPrisma = globalThis as unknown as {
|
|
11
|
+
prisma: PrismaClient | undefined;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
|
|
15
|
+
log: process.env.NODE_ENV === 'development'
|
|
16
|
+
? ['query', 'error', 'warn']
|
|
17
|
+
: ['error'],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
21
|
+
globalForPrisma.prisma = prisma;
|
|
22
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import type { NextRequest } from 'next/server';
|
|
3
|
+
import { getSessionCookie } from 'better-auth/cookies';
|
|
4
|
+
|
|
5
|
+
export function middleware(request: NextRequest) {
|
|
6
|
+
const sessionCookie = getSessionCookie(request);
|
|
7
|
+
const { pathname } = request.nextUrl;
|
|
8
|
+
|
|
9
|
+
const isProtectedRoute = pathname.startsWith('/dashboard');
|
|
10
|
+
const isAuthRoute = pathname === '/login' || pathname === '/register';
|
|
11
|
+
|
|
12
|
+
// Redirect to login if no session on protected routes
|
|
13
|
+
if (isProtectedRoute && !sessionCookie) {
|
|
14
|
+
return NextResponse.redirect(new URL('/login', request.url));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Redirect to dashboard if has session on auth routes
|
|
18
|
+
if (isAuthRoute && sessionCookie) {
|
|
19
|
+
return NextResponse.redirect(new URL('/dashboard', request.url));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return NextResponse.next();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const config = {
|
|
26
|
+
matcher: ['/dashboard/:path*', '/login', '/register'],
|
|
27
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// This is your Prisma schema file for better-auth
|
|
2
|
+
// Learn more: https://pris.ly/d/prisma-schema
|
|
3
|
+
|
|
4
|
+
generator client {
|
|
5
|
+
provider = "prisma-client-js"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
datasource db {
|
|
9
|
+
provider = "postgresql" // Change to "sqlite" for local development
|
|
10
|
+
url = env("DATABASE_URL")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ============================================
|
|
14
|
+
// better-auth required models
|
|
15
|
+
// ============================================
|
|
16
|
+
|
|
17
|
+
model User {
|
|
18
|
+
id String @id
|
|
19
|
+
name String?
|
|
20
|
+
email String @unique
|
|
21
|
+
emailVerified Boolean @default(false)
|
|
22
|
+
image String?
|
|
23
|
+
createdAt DateTime @default(now())
|
|
24
|
+
updatedAt DateTime @updatedAt
|
|
25
|
+
|
|
26
|
+
sessions Session[]
|
|
27
|
+
accounts Account[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
model Session {
|
|
31
|
+
id String @id
|
|
32
|
+
userId String
|
|
33
|
+
token String @unique
|
|
34
|
+
expiresAt DateTime
|
|
35
|
+
ipAddress String?
|
|
36
|
+
userAgent String?
|
|
37
|
+
createdAt DateTime @default(now())
|
|
38
|
+
updatedAt DateTime @updatedAt
|
|
39
|
+
|
|
40
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
41
|
+
|
|
42
|
+
@@index([userId])
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
model Account {
|
|
46
|
+
id String @id
|
|
47
|
+
userId String
|
|
48
|
+
accountId String
|
|
49
|
+
providerId String
|
|
50
|
+
accessToken String?
|
|
51
|
+
refreshToken String?
|
|
52
|
+
accessTokenExpiresAt DateTime?
|
|
53
|
+
refreshTokenExpiresAt DateTime?
|
|
54
|
+
scope String?
|
|
55
|
+
idToken String?
|
|
56
|
+
password String?
|
|
57
|
+
createdAt DateTime @default(now())
|
|
58
|
+
updatedAt DateTime @updatedAt
|
|
59
|
+
|
|
60
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
61
|
+
|
|
62
|
+
@@unique([providerId, accountId])
|
|
63
|
+
@@index([userId])
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
model Verification {
|
|
67
|
+
id String @id
|
|
68
|
+
identifier String
|
|
69
|
+
value String
|
|
70
|
+
expiresAt DateTime
|
|
71
|
+
createdAt DateTime @default(now())
|
|
72
|
+
updatedAt DateTime @updatedAt
|
|
73
|
+
|
|
74
|
+
@@index([identifier])
|
|
75
|
+
}
|