Full Next.js 15 platform with tRPC, Prisma, PostgreSQL, NextAuth. Includes production Dockerfile (multi-stage, port 7600), docker-compose with registry-based image pull, Gitea Actions CI workflow, nginx config for portal.monaco-opc.com, deployment scripts, and DEPLOYMENT.md guide. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
94 lines
2.0 KiB
TypeScript
94 lines
2.0 KiB
TypeScript
import bcrypt from 'bcryptjs'
|
|
|
|
const SALT_ROUNDS = 12
|
|
|
|
/**
|
|
* Hash a password using bcrypt
|
|
*/
|
|
export async function hashPassword(password: string): Promise<string> {
|
|
return bcrypt.hash(password, SALT_ROUNDS)
|
|
}
|
|
|
|
/**
|
|
* Verify a password against a hash
|
|
*/
|
|
export async function verifyPassword(
|
|
password: string,
|
|
hash: string
|
|
): Promise<boolean> {
|
|
return bcrypt.compare(password, hash)
|
|
}
|
|
|
|
interface PasswordValidation {
|
|
valid: boolean
|
|
errors: string[]
|
|
}
|
|
|
|
/**
|
|
* Validate password meets requirements:
|
|
* - Minimum 8 characters
|
|
* - At least one uppercase letter
|
|
* - At least one lowercase letter
|
|
* - At least one number
|
|
*/
|
|
export function validatePassword(password: string): PasswordValidation {
|
|
const errors: string[] = []
|
|
|
|
if (password.length < 8) {
|
|
errors.push('Password must be at least 8 characters long')
|
|
}
|
|
|
|
if (!/[A-Z]/.test(password)) {
|
|
errors.push('Password must contain at least one uppercase letter')
|
|
}
|
|
|
|
if (!/[a-z]/.test(password)) {
|
|
errors.push('Password must contain at least one lowercase letter')
|
|
}
|
|
|
|
if (!/[0-9]/.test(password)) {
|
|
errors.push('Password must contain at least one number')
|
|
}
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get password strength score (0-4)
|
|
* 0 = very weak, 4 = very strong
|
|
*/
|
|
export function getPasswordStrength(password: string): {
|
|
score: number
|
|
label: 'Very Weak' | 'Weak' | 'Fair' | 'Strong' | 'Very Strong'
|
|
} {
|
|
let score = 0
|
|
|
|
// Length
|
|
if (password.length >= 8) score++
|
|
if (password.length >= 12) score++
|
|
|
|
// Character variety
|
|
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) score++
|
|
if (/[0-9]/.test(password)) score++
|
|
if (/[^a-zA-Z0-9]/.test(password)) score++
|
|
|
|
// Normalize to 0-4
|
|
const normalizedScore = Math.min(4, score)
|
|
|
|
const labels: Record<number, 'Very Weak' | 'Weak' | 'Fair' | 'Strong' | 'Very Strong'> = {
|
|
0: 'Very Weak',
|
|
1: 'Weak',
|
|
2: 'Fair',
|
|
3: 'Strong',
|
|
4: 'Very Strong',
|
|
}
|
|
|
|
return {
|
|
score: normalizedScore,
|
|
label: labels[normalizedScore],
|
|
}
|
|
}
|