mirror of
https://github.com/thomasnordquist/MQTT-Explorer.git
synced 2026-09-14 18:44:39 +00:00
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com> Co-authored-by: Thomas Nordquist <thomasnordquist@users.noreply.github.com>
130 lines
3.8 KiB
TypeScript
130 lines
3.8 KiB
TypeScript
import * as fs from 'fs'
|
|
import * as path from 'path'
|
|
import * as bcrypt from 'bcryptjs'
|
|
import * as crypto from 'crypto'
|
|
import { v4 as uuidv4 } from 'uuid'
|
|
|
|
export interface Credentials {
|
|
username: string
|
|
passwordHash: string
|
|
}
|
|
|
|
export class AuthManager {
|
|
private credentialsPath: string
|
|
|
|
private credentials: Credentials | undefined
|
|
|
|
private skipAuth: boolean
|
|
|
|
constructor(credentialsPath: string) {
|
|
this.credentialsPath = credentialsPath
|
|
this.skipAuth = process.env.MQTT_EXPLORER_SKIP_AUTH === 'true'
|
|
}
|
|
|
|
public isAuthDisabled(): boolean {
|
|
return this.skipAuth
|
|
}
|
|
|
|
public async initialize(): Promise<void> {
|
|
const isProduction = process.env.NODE_ENV === 'production'
|
|
|
|
// Check if authentication is disabled
|
|
if (this.skipAuth) {
|
|
console.log('='.repeat(60))
|
|
console.log('WARNING: Authentication is DISABLED')
|
|
console.log('MQTT_EXPLORER_SKIP_AUTH=true')
|
|
console.log('This should only be used behind a secure authentication proxy!')
|
|
console.log('='.repeat(60))
|
|
return
|
|
}
|
|
|
|
// Try to get credentials from environment variables
|
|
const envUsername = process.env.MQTT_EXPLORER_USERNAME
|
|
const envPassword = process.env.MQTT_EXPLORER_PASSWORD
|
|
|
|
if (envUsername && envPassword) {
|
|
// Use environment credentials
|
|
if (!isProduction) {
|
|
console.log('Using credentials from environment variables')
|
|
console.log('Username:', envUsername)
|
|
}
|
|
this.credentials = {
|
|
username: envUsername,
|
|
passwordHash: await bcrypt.hash(envPassword, 10),
|
|
}
|
|
return
|
|
}
|
|
|
|
// Try to load from file
|
|
if (fs.existsSync(this.credentialsPath)) {
|
|
try {
|
|
const data = fs.readFileSync(this.credentialsPath, 'utf8')
|
|
this.credentials = JSON.parse(data)
|
|
if (!isProduction && this.credentials) {
|
|
console.log('Loaded credentials from', this.credentialsPath)
|
|
console.log('Username:', this.credentials.username)
|
|
}
|
|
return
|
|
} catch (error) {
|
|
console.error('Failed to load credentials from file:', error)
|
|
}
|
|
}
|
|
|
|
// Generate new credentials
|
|
const username = `user-${uuidv4().substring(0, 8)}`
|
|
const password = uuidv4()
|
|
|
|
console.log('='.repeat(60))
|
|
console.log('Generated new credentials:')
|
|
console.log('Username:', username)
|
|
console.log('Password:', password)
|
|
console.log('='.repeat(60))
|
|
console.log('Please save these credentials. They will be persisted to:')
|
|
console.log(this.credentialsPath)
|
|
console.log('='.repeat(60))
|
|
console.log('IMPORTANT: In production, use environment variables:')
|
|
console.log('export MQTT_EXPLORER_USERNAME=<username>')
|
|
console.log('export MQTT_EXPLORER_PASSWORD=<password>')
|
|
console.log('='.repeat(60))
|
|
|
|
this.credentials = {
|
|
username,
|
|
passwordHash: await bcrypt.hash(password, 10),
|
|
}
|
|
|
|
// Save to file
|
|
try {
|
|
const dir = path.dirname(this.credentialsPath)
|
|
if (!fs.existsSync(dir)) {
|
|
fs.mkdirSync(dir, { recursive: true })
|
|
}
|
|
fs.writeFileSync(this.credentialsPath, JSON.stringify(this.credentials, null, 2))
|
|
console.log('Credentials saved successfully')
|
|
} catch (error) {
|
|
console.error('Failed to save credentials:', error)
|
|
}
|
|
}
|
|
|
|
public async verifyCredentials(username: string, password: string): Promise<boolean> {
|
|
if (!this.credentials) {
|
|
return false
|
|
}
|
|
|
|
// Use constant-time comparison for username to prevent timing attacks
|
|
const usernameMatch = crypto.timingSafeEqual(
|
|
Buffer.from(username.padEnd(256, '\0')),
|
|
Buffer.from(this.credentials.username.padEnd(256, '\0'))
|
|
)
|
|
|
|
if (!usernameMatch) {
|
|
return false
|
|
}
|
|
|
|
return bcrypt.compare(password, this.credentials.passwordHash)
|
|
}
|
|
|
|
public getUsername(): string | undefined {
|
|
return this.credentials?.username
|
|
}
|
|
}
|