Files
basil/packages/api/src/scripts/create-admin.ts
Paul R Kartchner 2d53b9e283
Some checks failed
CI Pipeline / Lint Code (push) Has been cancelled
CI Pipeline / Test API Package (push) Has been cancelled
CI Pipeline / Test Web Package (push) Has been cancelled
CI Pipeline / Test Shared Package (push) Has been cancelled
CI Pipeline / Build All Packages (push) Has been cancelled
CI Pipeline / Generate Coverage Report (push) Has been cancelled
Docker Build & Deploy / Build Docker Images (push) Has been cancelled
Docker Build & Deploy / Push Docker Images (push) Has been cancelled
Docker Build & Deploy / Deploy to Staging (push) Has been cancelled
Docker Build & Deploy / Deploy to Production (push) Has been cancelled
E2E Tests / End-to-End Tests (push) Has been cancelled
E2E Tests / E2E Tests (Mobile) (push) Has been cancelled
Security Scanning / NPM Audit (push) Has been cancelled
Security Scanning / Dependency License Check (push) Has been cancelled
Security Scanning / Code Quality Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
feat: add comprehensive authentication system with JWT and OAuth
Implement a complete authentication system with local email/password
authentication, Google OAuth, JWT tokens, and role-based access control.

Backend Features:
- Database schema with User, RefreshToken, VerificationToken, RecipeShare models
- Role-based access control (USER, ADMIN)
- Recipe visibility controls (PRIVATE, SHARED, PUBLIC)
- Email verification for local accounts
- Password reset functionality
- JWT access tokens (15min) and refresh tokens (7 days)
- Passport.js strategies: Local, JWT, Google OAuth
- bcrypt password hashing with 12 salt rounds
- Password strength validation (min 8 chars, uppercase, lowercase, number)
- Rate limiting on auth endpoints (5 attempts/15min)
- Email service with styled HTML templates for verification and password reset

API Endpoints:
- POST /api/auth/register - Register with email/password
- POST /api/auth/login - Login and get tokens
- POST /api/auth/logout - Invalidate refresh token
- POST /api/auth/refresh - Get new access token
- GET /api/auth/verify-email/:token - Verify email address
- POST /api/auth/resend-verification - Resend verification email
- POST /api/auth/forgot-password - Request password reset
- POST /api/auth/reset-password - Reset password with token
- GET /api/auth/google - Initiate Google OAuth
- GET /api/auth/google/callback - Google OAuth callback
- GET /api/auth/me - Get current user info

Security Middleware:
- requireAuth - Protect routes requiring authentication
- requireAdmin - Admin-only route protection
- optionalAuth - Routes that work with or without auth
- requireOwnership - Check resource ownership

Admin Tools:
- npm run create-admin - Interactive script to create admin users
- verify-user-manual.ts - Helper script for testing

Test Coverage:
- 49 unit and integration tests (all passing)
- Password utility tests (12 tests)
- JWT utility tests (17 tests)
- Auth middleware tests (12 tests)
- Auth routes integration tests (8 tests)

Dependencies Added:
- passport, passport-local, passport-jwt, passport-google-oauth20
- bcrypt, jsonwebtoken
- nodemailer
- express-rate-limit, express-validator, cookie-parser

Environment Variables Required:
- JWT_SECRET, JWT_REFRESH_SECRET
- JWT_EXPIRES_IN, JWT_REFRESH_EXPIRES_IN
- GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET (optional)
- SMTP configuration for email
- APP_URL, API_URL

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 04:37:05 +00:00

121 lines
3.5 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Script to create an admin user
* Usage: npm run create-admin
* or: npx tsx src/scripts/create-admin.ts
*/
import { PrismaClient } from '@prisma/client';
import * as readline from 'readline';
import { hashPassword, validatePasswordStrength } from '../utils/password';
const prisma = new PrismaClient();
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
function question(query: string): Promise<string> {
return new Promise((resolve) => {
rl.question(query, resolve);
});
}
async function createAdmin() {
try {
console.log('\n🌿 Basil Recipe Manager - Create Admin User\n');
// Get email
const email = await question('Email address: ');
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
console.error('❌ Invalid email address');
process.exit(1);
}
// Check if user already exists
const existingUser = await prisma.user.findUnique({
where: { email: email.toLowerCase() },
});
if (existingUser) {
console.error('❌ User with this email already exists');
if (existingUser.role === 'ADMIN') {
console.log(' This user is already an admin');
} else {
const upgrade = await question('Would you like to upgrade this user to admin? (yes/no): ');
if (upgrade.toLowerCase() === 'yes' || upgrade.toLowerCase() === 'y') {
await prisma.user.update({
where: { id: existingUser.id },
data: { role: 'ADMIN' },
});
console.log('✅ User upgraded to admin successfully');
}
}
process.exit(0);
}
// Get name
const name = await question('Full name (optional): ');
// Get password
const password = await question('Password (min 8 chars, uppercase, lowercase, number): ');
// Validate password
const passwordValidation = validatePasswordStrength(password);
if (!passwordValidation.valid) {
console.error('❌ Password does not meet requirements:');
passwordValidation.errors.forEach((error) => {
console.error(` - ${error}`);
});
process.exit(1);
}
// Confirm password
const passwordConfirm = await question('Confirm password: ');
if (password !== passwordConfirm) {
console.error('❌ Passwords do not match');
process.exit(1);
}
// Hash password
console.log('\n🔐 Hashing password...');
const passwordHash = await hashPassword(password);
// Create admin user
console.log('👤 Creating admin user...');
const admin = await prisma.user.create({
data: {
email: email.toLowerCase(),
name: name || undefined,
passwordHash,
role: 'ADMIN',
provider: 'local',
emailVerified: true, // Auto-verify admin users
emailVerifiedAt: new Date(),
},
});
console.log('\n✅ Admin user created successfully!');
console.log('\nUser details:');
console.log(` ID: ${admin.id}`);
console.log(` Email: ${admin.email}`);
console.log(` Name: ${admin.name || 'N/A'}`);
console.log(` Role: ${admin.role}`);
console.log(` Email Verified: ${admin.emailVerified}`);
console.log(` Created: ${admin.createdAt.toISOString()}`);
console.log('\n🔑 You can now log in with this email and password\n');
} catch (error) {
console.error('\n❌ Error creating admin user:', error);
process.exit(1);
} finally {
rl.close();
await prisma.$disconnect();
}
}
createAdmin();