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 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;