nextjs-auth-module 1.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/README.md +147 -0
- package/bin/create-auth-app.js +46 -0
- package/bin/init-db.js +128 -0
- package/bin/setup-auth.js +156 -0
- package/package.json +56 -0
- package/sql/schema.sql +144 -0
- package/src/app/api/forgot-password/route.js +79 -0
- package/src/app/api/login/route.js +53 -0
- package/src/app/api/register/route.js +52 -0
- package/src/app/api/reset-password/route.js +91 -0
- package/src/app/forgot-password/page.jsx +126 -0
- package/src/app/login/page.jsx +255 -0
- package/src/app/register/page.jsx +340 -0
- package/src/app/reset-password/page.jsx +179 -0
- package/src/context/AuthContext.jsx +153 -0
- package/src/index.js +5 -0
- package/src/lib/auth.js +42 -0
- package/src/lib/db.js +7 -0
- package/src/lib/email.js +106 -0
- package/src/scripts/init-db.js +47 -0
- package/templates/.env.example +10 -0
- package/templates/layout.jsx +14 -0
- package/templates/page.jsx +289 -0
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Next.js Auth Module
|
|
2
|
+
|
|
3
|
+
A complete, production-ready authentication module for Next.js applications with PostgreSQL.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ User registration with email/password
|
|
8
|
+
- ✅ User login with JWT tokens
|
|
9
|
+
- ✅ Password reset with email notifications
|
|
10
|
+
- ✅ Protected routes
|
|
11
|
+
- ✅ PostgreSQL database support (Neon, AWS RDS, etc.)
|
|
12
|
+
- ✅ JWT authentication
|
|
13
|
+
- ✅ Bcrypt password hashing
|
|
14
|
+
- ✅ Ready-to-use components
|
|
15
|
+
- ✅ TypeScript ready (including .d.ts files)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
### Quick Setup (Recommended)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
1 npx create-next-app@latest my-app
|
|
23
|
+
cd my-app
|
|
24
|
+
npm install nextjs-auth-module
|
|
25
|
+
npx setup-auth
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# When prompted:
|
|
29
|
+
# ✓ Would you like to use TypeScript? … No
|
|
30
|
+
# ✓ Would you like to use ESLint? … Yes
|
|
31
|
+
# ✓ Would you like to use Tailwind CSS? … Yes
|
|
32
|
+
# ✓ Would you like to use `src/` directory? … No
|
|
33
|
+
# ✓ Would you like to use App Router? … Yes
|
|
34
|
+
# ✓ Would you like to customize the default import alias (@/*)? … No
|
|
35
|
+
|
|
36
|
+
# Navigate into the new project
|
|
37
|
+
cd my-new-auth-app
|
|
38
|
+
|
|
39
|
+
3. # Install all authentication dependencies
|
|
40
|
+
npm install bcryptjs jsonwebtoken pg nodemailer @heroicons/react dotenv
|
|
41
|
+
|
|
42
|
+
# Install dev dependencies (if needed)
|
|
43
|
+
npm install -D @types/bcryptjs @types/jsonwebtoken
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
4. # Install the auth module package
|
|
48
|
+
npm install nextjs-auth-package
|
|
49
|
+
|
|
50
|
+
# Verify it worked
|
|
51
|
+
npm list --depth=0 | grep nextjs-auth
|
|
52
|
+
# Should show: nextjs-auth-package@1.0.0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
5. # Run the setup to copy all auth files
|
|
56
|
+
npx setup-auth
|
|
57
|
+
|
|
58
|
+
# Or if that doesn't work, run:
|
|
59
|
+
node node_modules/nextjs-auth-package/bin/setup-auth.js
|
|
60
|
+
|
|
61
|
+
6. 7 . create database
|
|
62
|
+
|
|
63
|
+
run schema.sql file on sql editor on neon
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
7. # Create .env.local from example
|
|
67
|
+
|
|
68
|
+
copy file from .env.example to .env.local
|
|
69
|
+
|
|
70
|
+
and edit en.local to real data such as db URL and all place holder file with real data
|
|
71
|
+
|
|
72
|
+
# To generate JWT secret
|
|
73
|
+
run in bash
|
|
74
|
+
|
|
75
|
+
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
|
76
|
+
|
|
77
|
+
8 . npm run dev
|
|
78
|
+
Open http://localhost:3000
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
📁 Project Structure
|
|
82
|
+
text
|
|
83
|
+
your-project/
|
|
84
|
+
├── app/
|
|
85
|
+
│ ├── api/
|
|
86
|
+
│ │ ├── login/route.js
|
|
87
|
+
│ │ ├── register/route.js
|
|
88
|
+
│ │ ├── forgot-password/route.js
|
|
89
|
+
│ │ └── reset-password/route.js
|
|
90
|
+
│ ├── login/page.jsx
|
|
91
|
+
│ ├── register/page.jsx
|
|
92
|
+
│ ├── forgot-password/page.jsx
|
|
93
|
+
│ ├── reset-password/page.jsx
|
|
94
|
+
│ └── layout.jsx
|
|
95
|
+
├── context/
|
|
96
|
+
│ └── AuthContext.jsx
|
|
97
|
+
├── lib/
|
|
98
|
+
│ ├── db.js
|
|
99
|
+
│ ├── auth.js
|
|
100
|
+
│ └── email.js
|
|
101
|
+
├── scripts/
|
|
102
|
+
│ └── init-db.js
|
|
103
|
+
├── .env.local
|
|
104
|
+
└── package.json
|
|
105
|
+
|
|
106
|
+
📧 Email Configuration (Gmail)
|
|
107
|
+
To enable password reset emails:
|
|
108
|
+
|
|
109
|
+
Enable 2-Step Verification on your Google account
|
|
110
|
+
|
|
111
|
+
Generate an App Password:
|
|
112
|
+
|
|
113
|
+
Go to https://myaccount.google.com/apppasswords
|
|
114
|
+
|
|
115
|
+
Select "Other" and name it "NextJS Auth"
|
|
116
|
+
|
|
117
|
+
Copy the 16-character password
|
|
118
|
+
|
|
119
|
+
Update .env.local:
|
|
120
|
+
|
|
121
|
+
env
|
|
122
|
+
EMAIL_USER="your-email@gmail.com"
|
|
123
|
+
EMAIL_PASS="16-character-app-password"
|
|
124
|
+
🧪 Development Without Email
|
|
125
|
+
For testing without email, the reset link will be printed in your terminal:
|
|
126
|
+
|
|
127
|
+
text
|
|
128
|
+
🔐 ========== PASSWORD RESET LINK ==========
|
|
129
|
+
Reset URL: http://localhost:3000/reset-password?token=abc123
|
|
130
|
+
==========================================
|
|
131
|
+
🚢 Deployment
|
|
132
|
+
Deploy to Vercel
|
|
133
|
+
Push your code to GitHub
|
|
134
|
+
|
|
135
|
+
Import project to Vercel
|
|
136
|
+
|
|
137
|
+
Add environment variables in Vercel dashboard
|
|
138
|
+
|
|
139
|
+
Deploy
|
|
140
|
+
|
|
141
|
+
Environment Variables on Vercel
|
|
142
|
+
env
|
|
143
|
+
DATABASE_URL=your_neon_postgres_url
|
|
144
|
+
JWT_SECRET=your_jwt_secret
|
|
145
|
+
EMAIL_USER=your_email@gmail.com
|
|
146
|
+
EMAIL_PASS=your_app_password
|
|
147
|
+
NEXT_PUBLIC_APP_URL=https://your-domain.vercel.app
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const projectName = process.argv[2];
|
|
8
|
+
|
|
9
|
+
if (!projectName) {
|
|
10
|
+
console.error('\x1b[31m❌ Please specify a project name:\x1b[0m');
|
|
11
|
+
console.log('\x1b[33m npx create-auth-app my-app\x1b[0m\n');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
console.log('\x1b[36m%s\x1b[0m', `\n🚀 Creating Next.js app with authentication: ${projectName}\n`);
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// Create Next.js app
|
|
19
|
+
console.log('\x1b[33m📦 Creating Next.js app...\x1b[0m');
|
|
20
|
+
execSync(`npx create-next-app@latest ${projectName} --typescript --tailwind --app --no-src-dir`, {
|
|
21
|
+
stdio: 'inherit'
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Install auth dependencies
|
|
25
|
+
console.log('\x1b[33m📦 Installing auth dependencies...\x1b[0m');
|
|
26
|
+
execSync(`cd ${projectName} && npm install bcryptjs jsonwebtoken pg nodemailer dotenv @heroicons/react`, {
|
|
27
|
+
stdio: 'inherit'
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Setup auth module
|
|
31
|
+
console.log('\x1b[33m🔧 Setting up authentication module...\x1b[0m');
|
|
32
|
+
execSync(`cd ${projectName} && npx setup-auth`, {
|
|
33
|
+
stdio: 'inherit'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
console.log('\x1b[32m✅ Authentication setup complete!\x1b[0m');
|
|
37
|
+
console.log('\n📝 Next steps:');
|
|
38
|
+
console.log(`\x1b[33m1. cd ${projectName}\x1b[0m`);
|
|
39
|
+
console.log('\x1b[33m2. cp .env.example .env.local\x1b[0m');
|
|
40
|
+
console.log('\x1b[33m3. Update .env.local with your database credentials\x1b[0m');
|
|
41
|
+
console.log('\x1b[33m4. npm run dev\x1b[0m\n');
|
|
42
|
+
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('\x1b[31m❌ Setup failed:\x1b[0m', error.message);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
package/bin/init-db.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Pool } = require('pg');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
require('dotenv').config({ path: '.env.local' });
|
|
8
|
+
|
|
9
|
+
const rl = readline.createInterface({
|
|
10
|
+
input: process.stdin,
|
|
11
|
+
output: process.stdout
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
console.log('\x1b[36m%s\x1b[0m', '🗄️ Next.js Auth Module - Database Setup\n');
|
|
15
|
+
|
|
16
|
+
// Check if DATABASE_URL exists
|
|
17
|
+
if (!process.env.DATABASE_URL) {
|
|
18
|
+
console.log('\x1b[31m❌ DATABASE_URL not found in .env.local\x1b[0m');
|
|
19
|
+
console.log('\x1b[33m📝 Please create .env.local file with:\x1b[0m');
|
|
20
|
+
console.log('DATABASE_URL="postgresql://username:password@host.neon.tech/dbname?sslmode=require"\n');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const pool = new Pool({
|
|
25
|
+
connectionString: process.env.DATABASE_URL,
|
|
26
|
+
ssl: {
|
|
27
|
+
rejectUnauthorized: false
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
async function initDatabase() {
|
|
32
|
+
const client = await pool.connect();
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
console.log('\x1b[33m📡 Connecting to database...\x1b[0m');
|
|
36
|
+
await client.query('SELECT NOW()');
|
|
37
|
+
console.log('\x1b[32m✅ Database connected successfully!\x1b[0m\n');
|
|
38
|
+
|
|
39
|
+
// Ask which SQL to run
|
|
40
|
+
console.log('\x1b[33m📁 Available SQL files:\x1b[0m');
|
|
41
|
+
console.log(' 1. schema.sql - Complete database schema');
|
|
42
|
+
console.log(' 2. migrations/001_initial.sql - Basic tables only');
|
|
43
|
+
console.log(' 3. migrations/003_add_indexes.sql - Add performance indexes');
|
|
44
|
+
console.log(' 4. Run all migrations in order');
|
|
45
|
+
|
|
46
|
+
rl.question('\n\x1b[36mChoose option (1-4): \x1b[0m', async (answer) => {
|
|
47
|
+
let sqlContent = '';
|
|
48
|
+
const packageDir = path.dirname(__dirname);
|
|
49
|
+
const sqlDir = path.join(packageDir, 'sql');
|
|
50
|
+
|
|
51
|
+
switch(answer) {
|
|
52
|
+
case '1':
|
|
53
|
+
sqlContent = fs.readFileSync(path.join(sqlDir, 'schema.sql'), 'utf8');
|
|
54
|
+
await runSQL(client, sqlContent);
|
|
55
|
+
break;
|
|
56
|
+
case '2':
|
|
57
|
+
sqlContent = fs.readFileSync(path.join(sqlDir, 'migrations/001_initial.sql'), 'utf8');
|
|
58
|
+
await runSQL(client, sqlContent);
|
|
59
|
+
break;
|
|
60
|
+
case '3':
|
|
61
|
+
sqlContent = fs.readFileSync(path.join(sqlDir, 'migrations/003_add_indexes.sql'), 'utf8');
|
|
62
|
+
await runSQL(client, sqlContent);
|
|
63
|
+
break;
|
|
64
|
+
case '4':
|
|
65
|
+
await runAllMigrations(client, sqlDir);
|
|
66
|
+
break;
|
|
67
|
+
default:
|
|
68
|
+
console.log('\x1b[31m❌ Invalid option\x1b[0m');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
rl.close();
|
|
72
|
+
await pool.end();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('\x1b[31m❌ Database initialization failed:\x1b[0m', error.message);
|
|
77
|
+
rl.close();
|
|
78
|
+
await pool.end();
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function runSQL(client, sqlContent) {
|
|
84
|
+
try {
|
|
85
|
+
console.log('\x1b[33m🚀 Running SQL...\x1b[0m');
|
|
86
|
+
await client.query(sqlContent);
|
|
87
|
+
console.log('\x1b[32m✅ Database schema created successfully!\x1b[0m');
|
|
88
|
+
|
|
89
|
+
// Verify tables
|
|
90
|
+
const { rows } = await client.query(`
|
|
91
|
+
SELECT table_name
|
|
92
|
+
FROM information_schema.tables
|
|
93
|
+
WHERE table_schema = 'public'
|
|
94
|
+
AND table_name IN ('users', 'sessions')
|
|
95
|
+
`);
|
|
96
|
+
|
|
97
|
+
console.log('\n\x1b[36m📊 Created tables:\x1b[0m', rows.map(r => r.table_name).join(', '));
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('\x1b[31m❌ Error running SQL:\x1b[0m', error.message);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function runAllMigrations(client, sqlDir) {
|
|
105
|
+
const migrationsDir = path.join(sqlDir, 'migrations');
|
|
106
|
+
const migrationFiles = fs.readdirSync(migrationsDir)
|
|
107
|
+
.filter(f => f.endsWith('.sql'))
|
|
108
|
+
.sort();
|
|
109
|
+
|
|
110
|
+
console.log(`\n\x1b[33m📦 Running ${migrationFiles.length} migrations...\x1b[0m`);
|
|
111
|
+
|
|
112
|
+
for (const file of migrationFiles) {
|
|
113
|
+
console.log(` → Running ${file}...`);
|
|
114
|
+
const sqlContent = fs.readFileSync(path.join(migrationsDir, file), 'utf8');
|
|
115
|
+
await client.query(sqlContent);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log('\n\x1b[32m✅ All migrations completed!\x1b[0m');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Handle process termination
|
|
122
|
+
process.on('SIGINT', () => {
|
|
123
|
+
console.log('\n\x1b[33m⚠️ Database setup cancelled\x1b[0m');
|
|
124
|
+
rl.close();
|
|
125
|
+
process.exit(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
initDatabase();
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
console.log('\x1b[36m%s\x1b[0m', '🔐 Setting up Next.js Auth Module...\n');
|
|
7
|
+
|
|
8
|
+
// Get the current working directory (the user's project)
|
|
9
|
+
const targetPath = process.cwd();
|
|
10
|
+
|
|
11
|
+
// Get the package directory
|
|
12
|
+
const packageDir = path.dirname(__dirname);
|
|
13
|
+
|
|
14
|
+
// Source directories
|
|
15
|
+
const srcAppDir = path.join(packageDir, 'src', 'app');
|
|
16
|
+
const srcContextDir = path.join(packageDir, 'src', 'context');
|
|
17
|
+
const srcLibDir = path.join(packageDir, 'src', 'lib');
|
|
18
|
+
const templatesDir = path.join(packageDir, 'templates');
|
|
19
|
+
|
|
20
|
+
// Target directories
|
|
21
|
+
const targetAppDir = path.join(targetPath, 'app');
|
|
22
|
+
const targetContextDir = path.join(targetPath, 'context');
|
|
23
|
+
const targetLibDir = path.join(targetPath, 'lib');
|
|
24
|
+
|
|
25
|
+
// Function to copy directory recursively
|
|
26
|
+
function copyDir(src, dest) {
|
|
27
|
+
if (!fs.existsSync(src)) {
|
|
28
|
+
console.log(`\x1b[31m❌ Source directory not found: ${src}\x1b[0m`);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Create destination directory
|
|
33
|
+
if (!fs.existsSync(dest)) {
|
|
34
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
38
|
+
|
|
39
|
+
for (let entry of entries) {
|
|
40
|
+
const srcPath = path.join(src, entry.name);
|
|
41
|
+
const destPath = path.join(dest, entry.name);
|
|
42
|
+
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
copyDir(srcPath, destPath);
|
|
45
|
+
} else {
|
|
46
|
+
// Don't overwrite existing files
|
|
47
|
+
if (!fs.existsSync(destPath)) {
|
|
48
|
+
fs.copyFileSync(srcPath, destPath);
|
|
49
|
+
console.log(` ✓ Copied: ${path.relative(targetPath, destPath)}`);
|
|
50
|
+
} else {
|
|
51
|
+
console.log(` ⚠️ Skipped (exists): ${path.relative(targetPath, destPath)}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Copy app directory (API routes and pages)
|
|
59
|
+
console.log('\x1b[33m📁 Copying authentication pages and API routes...\x1b[0m');
|
|
60
|
+
if (copyDir(srcAppDir, targetAppDir)) {
|
|
61
|
+
console.log('\x1b[32m✅ Pages and API routes copied\x1b[0m');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Copy context directory
|
|
65
|
+
console.log('\x1b[33m📁 Copying authentication context...\x1b[0m');
|
|
66
|
+
if (copyDir(srcContextDir, targetContextDir)) {
|
|
67
|
+
console.log('\x1b[32m✅ Auth context copied\x1b[0m');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Copy lib directory
|
|
71
|
+
console.log('\x1b[33m📁 Copying authentication utilities...\x1b[0m');
|
|
72
|
+
if (copyDir(srcLibDir, targetLibDir)) {
|
|
73
|
+
console.log('\x1b[32m✅ Auth utilities copied\x1b[0m');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// === NEW: Copy layout.jsx and page.jsx templates ===
|
|
77
|
+
console.log('\n\x1b[33m📄 Setting up layout and page files...\x1b[0m');
|
|
78
|
+
|
|
79
|
+
// Copy layout.jsx if it doesn't exist
|
|
80
|
+
const layoutTemplate = path.join(templatesDir, 'layout.jsx');
|
|
81
|
+
const layoutTarget = path.join(targetAppDir, 'layout.jsx');
|
|
82
|
+
|
|
83
|
+
if (fs.existsSync(layoutTemplate)) {
|
|
84
|
+
if (!fs.existsSync(layoutTarget)) {
|
|
85
|
+
fs.copyFileSync(layoutTemplate, layoutTarget);
|
|
86
|
+
console.log(' ✓ Created app/layout.jsx');
|
|
87
|
+
} else {
|
|
88
|
+
console.log(' ⚠️ app/layout.jsx already exists, skipping (check if AuthProvider is included)');
|
|
89
|
+
|
|
90
|
+
// Check if AuthProvider is in the existing layout
|
|
91
|
+
const layoutContent = fs.readFileSync(layoutTarget, 'utf8');
|
|
92
|
+
if (!layoutContent.includes('AuthProvider')) {
|
|
93
|
+
console.log(' 💡 Tip: Make sure to wrap your app with <AuthProvider> in layout.jsx');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Copy page.jsx if it doesn't exist
|
|
99
|
+
const pageTemplate = path.join(templatesDir, 'page.jsx');
|
|
100
|
+
const pageTarget = path.join(targetAppDir, 'page.jsx');
|
|
101
|
+
|
|
102
|
+
if (fs.existsSync(pageTemplate)) {
|
|
103
|
+
if (!fs.existsSync(pageTarget)) {
|
|
104
|
+
fs.copyFileSync(pageTemplate, pageTarget);
|
|
105
|
+
console.log(' ✓ Created app/page.jsx');
|
|
106
|
+
} else {
|
|
107
|
+
console.log(' ⚠️ app/page.jsx already exists, skipping');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Copy .env.example
|
|
112
|
+
console.log('\n\x1b[33m📝 Setting up environment variables...\x1b[0m');
|
|
113
|
+
const envExampleSrc = path.join(templatesDir, '.env.example');
|
|
114
|
+
const envExampleDest = path.join(targetPath, '.env.example');
|
|
115
|
+
|
|
116
|
+
if (fs.existsSync(envExampleSrc)) {
|
|
117
|
+
if (!fs.existsSync(envExampleDest)) {
|
|
118
|
+
fs.copyFileSync(envExampleSrc, envExampleDest);
|
|
119
|
+
console.log(' ✓ Created .env.example');
|
|
120
|
+
} else {
|
|
121
|
+
console.log(' ⚠️ .env.example already exists');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Copy .env.local from example if it doesn't exist
|
|
126
|
+
const envLocalDest = path.join(targetPath, '.env.local');
|
|
127
|
+
if (!fs.existsSync(envLocalDest) && fs.existsSync(envExampleSrc)) {
|
|
128
|
+
fs.copyFileSync(envExampleSrc, envLocalDest);
|
|
129
|
+
console.log(' ✓ Created .env.local (please update with your credentials)');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Copy SQL files
|
|
133
|
+
console.log('\n\x1b[33m🗄️ Copying database SQL files...\x1b[0m');
|
|
134
|
+
const sqlSrc = path.join(packageDir, 'sql');
|
|
135
|
+
const sqlDest = path.join(targetPath, 'sql');
|
|
136
|
+
|
|
137
|
+
if (fs.existsSync(sqlSrc)) {
|
|
138
|
+
copyDir(sqlSrc, sqlDest);
|
|
139
|
+
console.log('\x1b[32m✅ SQL files copied to /sql directory\x1b[0m');
|
|
140
|
+
console.log(' 📁 Run: node sql/init-db.js to setup database');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
console.log('\n\x1b[32m✨ Authentication module setup complete!\x1b[0m');
|
|
145
|
+
console.log('\n\x1b[33m📝 Next steps:\x1b[0m');
|
|
146
|
+
console.log(' 1. Update .env.local with your database credentials');
|
|
147
|
+
console.log(' 2. Make sure you have these dependencies installed:');
|
|
148
|
+
console.log(' npm install bcryptjs jsonwebtoken pg nodemailer @heroicons/react and npm install bcryptjs jsonwebtoken pg nodemailer @heroicons/react dotenv');
|
|
149
|
+
console.log(' 3. Initialize database: node scripts/init-db.js');
|
|
150
|
+
console.log(' 4. Run: npm run dev');
|
|
151
|
+
console.log('\n\x1b[36m📚 Available routes:\x1b[0m');
|
|
152
|
+
console.log(' - /login → Login page');
|
|
153
|
+
console.log(' - /register → Register page');
|
|
154
|
+
console.log(' - /forgot-password → Password reset request');
|
|
155
|
+
console.log(' - /reset-password → Reset password page');
|
|
156
|
+
console.log('\n');
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nextjs-auth-module",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Complete authentication module for Next.js with PostgreSQL, JWT, and password reset functionality",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"setup-auth": "./bin/setup-auth.js",
|
|
8
|
+
"init-db": "./bin/init-db.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"bin",
|
|
13
|
+
"sql",
|
|
14
|
+
"templates",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
|
|
19
|
+
"keywords": [
|
|
20
|
+
"nextjs",
|
|
21
|
+
"authentication",
|
|
22
|
+
"auth",
|
|
23
|
+
"login",
|
|
24
|
+
"register",
|
|
25
|
+
"password-reset",
|
|
26
|
+
"jwt",
|
|
27
|
+
"postgresql",
|
|
28
|
+
"neon",
|
|
29
|
+
"react"
|
|
30
|
+
],
|
|
31
|
+
"author": "Muler <mulercs514@gmail.com>",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/MulatuMekonnen/nextjs-auth-module"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/MulatuMekonnen/nextjs-auth-module#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/MulatuMekonnen/nextjs-auth-module/issues"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"next": ">=13.0.0",
|
|
43
|
+
"react": ">=18.0.0",
|
|
44
|
+
"react-dom": ">=18.0.0"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"bcryptjs": "^2.4.3",
|
|
48
|
+
"jsonwebtoken": "^9.0.0",
|
|
49
|
+
"pg": "^8.11.0",
|
|
50
|
+
"nodemailer": "^6.9.0",
|
|
51
|
+
"@heroicons/react": "^2.0.0"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=18.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/sql/schema.sql
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
-- ============================================
|
|
2
|
+
-- COMPLETE AUTHENTICATION MODULE SCHEMA
|
|
3
|
+
-- Run this entire script in Neon SQL Editor
|
|
4
|
+
-- ============================================
|
|
5
|
+
|
|
6
|
+
-- First, drop existing tables if you want a fresh start (optional)
|
|
7
|
+
-- DROP TABLE IF EXISTS sessions;
|
|
8
|
+
-- DROP TABLE IF EXISTS users;
|
|
9
|
+
|
|
10
|
+
-- ============================================
|
|
11
|
+
-- USERS TABLE (Enhanced with all auth fields)
|
|
12
|
+
-- ============================================
|
|
13
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
14
|
+
-- Basic Info
|
|
15
|
+
id SERIAL PRIMARY KEY,
|
|
16
|
+
name VARCHAR(100),
|
|
17
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
18
|
+
password VARCHAR(255) NOT NULL,
|
|
19
|
+
|
|
20
|
+
-- Email Verification
|
|
21
|
+
is_verified BOOLEAN DEFAULT FALSE,
|
|
22
|
+
verification_token TEXT,
|
|
23
|
+
verification_token_expires TIMESTAMP,
|
|
24
|
+
|
|
25
|
+
-- Password Reset
|
|
26
|
+
reset_password_token TEXT, -- matches your reset_token
|
|
27
|
+
reset_password_expires TIMESTAMP, -- matches your reset_token_expiry but as TIMESTAMP
|
|
28
|
+
|
|
29
|
+
-- Session Management (for refresh tokens)
|
|
30
|
+
-- Add these if you want remember me functionality
|
|
31
|
+
refresh_token TEXT,
|
|
32
|
+
refresh_token_expires TIMESTAMP,
|
|
33
|
+
|
|
34
|
+
-- Account Status
|
|
35
|
+
is_active BOOLEAN DEFAULT TRUE,
|
|
36
|
+
last_login TIMESTAMP,
|
|
37
|
+
login_attempts INTEGER DEFAULT 0,
|
|
38
|
+
locked_until TIMESTAMP,
|
|
39
|
+
|
|
40
|
+
-- Timestamps
|
|
41
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
42
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
-- ============================================
|
|
46
|
+
-- SESSIONS TABLE (For tracking user sessions)
|
|
47
|
+
-- ============================================
|
|
48
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
49
|
+
id SERIAL PRIMARY KEY,
|
|
50
|
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
51
|
+
session_token TEXT UNIQUE NOT NULL,
|
|
52
|
+
device_info TEXT,
|
|
53
|
+
ip_address VARCHAR(45),
|
|
54
|
+
expires_at TIMESTAMP NOT NULL,
|
|
55
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
56
|
+
last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
-- ============================================
|
|
60
|
+
-- PASSWORD RESET TOKENS TABLE (Alternative approach)
|
|
61
|
+
-- ============================================
|
|
62
|
+
-- This is optional but recommended for better organization
|
|
63
|
+
CREATE TABLE IF NOT EXISTS password_resets (
|
|
64
|
+
id SERIAL PRIMARY KEY,
|
|
65
|
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
66
|
+
token TEXT UNIQUE NOT NULL,
|
|
67
|
+
expires_at TIMESTAMP NOT NULL,
|
|
68
|
+
used BOOLEAN DEFAULT FALSE,
|
|
69
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
-- ============================================
|
|
73
|
+
-- INDEXES for better performance
|
|
74
|
+
-- ============================================
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_users_reset_token ON users(reset_password_token);
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_users_verification_token ON users(verification_token);
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(session_token);
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_password_resets_token ON password_resets(token);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_password_resets_user_id ON password_resets(user_id);
|
|
82
|
+
|
|
83
|
+
-- ============================================
|
|
84
|
+
-- AUTOMATIC UPDATED_AT TRIGGER
|
|
85
|
+
-- ============================================
|
|
86
|
+
-- Create function to update updated_at timestamp
|
|
87
|
+
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
|
88
|
+
RETURNS TRIGGER AS $$
|
|
89
|
+
BEGIN
|
|
90
|
+
NEW.updated_at = CURRENT_TIMESTAMP;
|
|
91
|
+
RETURN NEW;
|
|
92
|
+
END;
|
|
93
|
+
$$ language 'plpgsql';
|
|
94
|
+
|
|
95
|
+
-- Create trigger for users table
|
|
96
|
+
DROP TRIGGER IF EXISTS update_users_updated_at ON users;
|
|
97
|
+
CREATE TRIGGER update_users_updated_at
|
|
98
|
+
BEFORE UPDATE ON users
|
|
99
|
+
FOR EACH ROW
|
|
100
|
+
EXECUTE FUNCTION update_updated_at_column();
|
|
101
|
+
|
|
102
|
+
-- ============================================
|
|
103
|
+
-- SAMPLE DATA (Optional - for testing)
|
|
104
|
+
-- ============================================
|
|
105
|
+
-- Insert a test user (password is 'password123' hashed with bcrypt)
|
|
106
|
+
-- Remove this in production!
|
|
107
|
+
-- INSERT INTO users (name, email, password, is_verified)
|
|
108
|
+
-- VALUES ('Test User', 'test@example.com', '$2a$10$YourHashedPasswordHere', true);
|
|
109
|
+
|
|
110
|
+
-- ============================================
|
|
111
|
+
-- VERIFY SCHEMA
|
|
112
|
+
-- ============================================
|
|
113
|
+
-- Run this to check if all columns exist
|
|
114
|
+
SELECT column_name, data_type
|
|
115
|
+
FROM information_schema.columns
|
|
116
|
+
WHERE table_name = 'users'
|
|
117
|
+
ORDER BY ordinal_position;
|
|
118
|
+
|
|
119
|
+
-- ============================================
|
|
120
|
+
-- CLEANUP OLD TOKENS FUNCTION (Run daily)
|
|
121
|
+
-- ============================================
|
|
122
|
+
-- This function removes expired tokens
|
|
123
|
+
CREATE OR REPLACE FUNCTION cleanup_expired_tokens()
|
|
124
|
+
RETURNS void AS $$
|
|
125
|
+
BEGIN
|
|
126
|
+
-- Clear expired reset tokens in users table
|
|
127
|
+
UPDATE users
|
|
128
|
+
SET reset_password_token = NULL,
|
|
129
|
+
reset_password_expires = NULL
|
|
130
|
+
WHERE reset_password_expires < NOW();
|
|
131
|
+
|
|
132
|
+
-- Clear expired verification tokens
|
|
133
|
+
UPDATE users
|
|
134
|
+
SET verification_token = NULL,
|
|
135
|
+
verification_token_expires = NULL
|
|
136
|
+
WHERE verification_token_expires < NOW();
|
|
137
|
+
|
|
138
|
+
-- Delete expired sessions
|
|
139
|
+
DELETE FROM sessions WHERE expires_at < NOW();
|
|
140
|
+
|
|
141
|
+
-- Delete expired password resets
|
|
142
|
+
DELETE FROM password_resets WHERE expires_at < NOW() OR used = TRUE;
|
|
143
|
+
END;
|
|
144
|
+
$$ LANGUAGE plpgsql;
|