|
|
const express = require('express') |
|
|
const bcrypt = require('bcryptjs') |
|
|
const crypto = require('crypto') |
|
|
const path = require('path') |
|
|
const fs = require('fs') |
|
|
const redis = require('../models/redis') |
|
|
const logger = require('../utils/logger') |
|
|
const config = require('../../config/config') |
|
|
|
|
|
const router = express.Router() |
|
|
|
|
|
|
|
|
router.use('/assets', express.static(path.join(__dirname, '../../web/assets'))) |
|
|
|
|
|
|
|
|
router.get('/', (req, res) => { |
|
|
res.redirect(301, '/admin-next/api-stats') |
|
|
}) |
|
|
|
|
|
|
|
|
router.post('/auth/login', async (req, res) => { |
|
|
try { |
|
|
const { username, password } = req.body |
|
|
|
|
|
if (!username || !password) { |
|
|
return res.status(400).json({ |
|
|
error: 'Missing credentials', |
|
|
message: 'Username and password are required' |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
let adminData = await redis.getSession('admin_credentials') |
|
|
|
|
|
|
|
|
if (!adminData || Object.keys(adminData).length === 0) { |
|
|
const initFilePath = path.join(__dirname, '../../data/init.json') |
|
|
|
|
|
if (fs.existsSync(initFilePath)) { |
|
|
try { |
|
|
const initData = JSON.parse(fs.readFileSync(initFilePath, 'utf8')) |
|
|
const saltRounds = 10 |
|
|
const passwordHash = await bcrypt.hash(initData.adminPassword, saltRounds) |
|
|
|
|
|
adminData = { |
|
|
username: initData.adminUsername, |
|
|
passwordHash, |
|
|
createdAt: initData.initializedAt || new Date().toISOString(), |
|
|
lastLogin: null, |
|
|
updatedAt: initData.updatedAt || null |
|
|
} |
|
|
|
|
|
|
|
|
await redis.getClient().hset('session:admin_credentials', adminData) |
|
|
|
|
|
logger.info('✅ Admin credentials reloaded from init.json') |
|
|
} catch (error) { |
|
|
logger.error('❌ Failed to reload admin credentials:', error) |
|
|
return res.status(401).json({ |
|
|
error: 'Invalid credentials', |
|
|
message: 'Invalid username or password' |
|
|
}) |
|
|
} |
|
|
} else { |
|
|
return res.status(401).json({ |
|
|
error: 'Invalid credentials', |
|
|
message: 'Invalid username or password' |
|
|
}) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const isValidUsername = adminData.username === username |
|
|
const isValidPassword = await bcrypt.compare(password, adminData.passwordHash) |
|
|
|
|
|
if (!isValidUsername || !isValidPassword) { |
|
|
logger.security(`🔒 Failed login attempt for username: ${username}`) |
|
|
return res.status(401).json({ |
|
|
error: 'Invalid credentials', |
|
|
message: 'Invalid username or password' |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
const sessionId = crypto.randomBytes(32).toString('hex') |
|
|
|
|
|
|
|
|
const sessionData = { |
|
|
username: adminData.username, |
|
|
loginTime: new Date().toISOString(), |
|
|
lastActivity: new Date().toISOString() |
|
|
} |
|
|
|
|
|
await redis.setSession(sessionId, sessionData, config.security.adminSessionTimeout) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.success(`🔐 Admin login successful: ${username}`) |
|
|
|
|
|
return res.json({ |
|
|
success: true, |
|
|
token: sessionId, |
|
|
expiresIn: config.security.adminSessionTimeout, |
|
|
username: adminData.username |
|
|
}) |
|
|
} catch (error) { |
|
|
logger.error('❌ Login error:', error) |
|
|
return res.status(500).json({ |
|
|
error: 'Login failed', |
|
|
message: 'Internal server error' |
|
|
}) |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
router.post('/auth/logout', async (req, res) => { |
|
|
try { |
|
|
const token = req.headers['authorization']?.replace('Bearer ', '') || req.cookies?.adminToken |
|
|
|
|
|
if (token) { |
|
|
await redis.deleteSession(token) |
|
|
logger.success('🚪 Admin logout successful') |
|
|
} |
|
|
|
|
|
return res.json({ success: true, message: 'Logout successful' }) |
|
|
} catch (error) { |
|
|
logger.error('❌ Logout error:', error) |
|
|
return res.status(500).json({ |
|
|
error: 'Logout failed', |
|
|
message: 'Internal server error' |
|
|
}) |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
router.post('/auth/change-password', async (req, res) => { |
|
|
try { |
|
|
const token = req.headers['authorization']?.replace('Bearer ', '') || req.cookies?.adminToken |
|
|
|
|
|
if (!token) { |
|
|
return res.status(401).json({ |
|
|
error: 'No token provided', |
|
|
message: 'Authentication required' |
|
|
}) |
|
|
} |
|
|
|
|
|
const { newUsername, currentPassword, newPassword } = req.body |
|
|
|
|
|
if (!currentPassword || !newPassword) { |
|
|
return res.status(400).json({ |
|
|
error: 'Missing required fields', |
|
|
message: 'Current password and new password are required' |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
if (newPassword.length < 8) { |
|
|
return res.status(400).json({ |
|
|
error: 'Password too short', |
|
|
message: 'New password must be at least 8 characters long' |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
const sessionData = await redis.getSession(token) |
|
|
if (!sessionData) { |
|
|
return res.status(401).json({ |
|
|
error: 'Invalid token', |
|
|
message: 'Session expired or invalid' |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
const adminData = await redis.getSession('admin_credentials') |
|
|
if (!adminData) { |
|
|
return res.status(500).json({ |
|
|
error: 'Admin data not found', |
|
|
message: 'Administrator credentials not found' |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
const isValidPassword = await bcrypt.compare(currentPassword, adminData.passwordHash) |
|
|
if (!isValidPassword) { |
|
|
logger.security(`🔒 Invalid current password attempt for user: ${sessionData.username}`) |
|
|
return res.status(401).json({ |
|
|
error: 'Invalid current password', |
|
|
message: 'Current password is incorrect' |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
const updatedUsername = |
|
|
newUsername && newUsername.trim() ? newUsername.trim() : adminData.username |
|
|
|
|
|
|
|
|
const initFilePath = path.join(__dirname, '../../data/init.json') |
|
|
if (!fs.existsSync(initFilePath)) { |
|
|
return res.status(500).json({ |
|
|
error: 'Configuration file not found', |
|
|
message: 'init.json file is missing' |
|
|
}) |
|
|
} |
|
|
|
|
|
try { |
|
|
const initData = JSON.parse(fs.readFileSync(initFilePath, 'utf8')) |
|
|
|
|
|
|
|
|
|
|
|
initData.adminUsername = updatedUsername |
|
|
initData.adminPassword = newPassword |
|
|
initData.updatedAt = new Date().toISOString() |
|
|
|
|
|
|
|
|
fs.writeFileSync(initFilePath, JSON.stringify(initData, null, 2)) |
|
|
|
|
|
|
|
|
const saltRounds = 10 |
|
|
const newPasswordHash = await bcrypt.hash(newPassword, saltRounds) |
|
|
|
|
|
const updatedAdminData = { |
|
|
username: updatedUsername, |
|
|
passwordHash: newPasswordHash, |
|
|
createdAt: adminData.createdAt, |
|
|
lastLogin: adminData.lastLogin, |
|
|
updatedAt: new Date().toISOString() |
|
|
} |
|
|
|
|
|
await redis.setSession('admin_credentials', updatedAdminData) |
|
|
} catch (fileError) { |
|
|
logger.error('❌ Failed to update init.json:', fileError) |
|
|
return res.status(500).json({ |
|
|
error: 'Update failed', |
|
|
message: 'Failed to update configuration file' |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
await redis.deleteSession(token) |
|
|
|
|
|
logger.success(`🔐 Admin password changed successfully for user: ${updatedUsername}`) |
|
|
|
|
|
return res.json({ |
|
|
success: true, |
|
|
message: 'Password changed successfully. Please login again.', |
|
|
newUsername: updatedUsername |
|
|
}) |
|
|
} catch (error) { |
|
|
logger.error('❌ Change password error:', error) |
|
|
return res.status(500).json({ |
|
|
error: 'Change password failed', |
|
|
message: 'Internal server error' |
|
|
}) |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
router.get('/auth/user', async (req, res) => { |
|
|
try { |
|
|
const token = req.headers['authorization']?.replace('Bearer ', '') || req.cookies?.adminToken |
|
|
|
|
|
if (!token) { |
|
|
return res.status(401).json({ |
|
|
error: 'No token provided', |
|
|
message: 'Authentication required' |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
const sessionData = await redis.getSession(token) |
|
|
if (!sessionData) { |
|
|
return res.status(401).json({ |
|
|
error: 'Invalid token', |
|
|
message: 'Session expired or invalid' |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
const adminData = await redis.getSession('admin_credentials') |
|
|
if (!adminData) { |
|
|
return res.status(500).json({ |
|
|
error: 'Admin data not found', |
|
|
message: 'Administrator credentials not found' |
|
|
}) |
|
|
} |
|
|
|
|
|
return res.json({ |
|
|
success: true, |
|
|
user: { |
|
|
username: adminData.username, |
|
|
loginTime: sessionData.loginTime, |
|
|
lastActivity: sessionData.lastActivity |
|
|
} |
|
|
}) |
|
|
} catch (error) { |
|
|
logger.error('❌ Get user info error:', error) |
|
|
return res.status(500).json({ |
|
|
error: 'Get user info failed', |
|
|
message: 'Internal server error' |
|
|
}) |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
router.post('/auth/refresh', async (req, res) => { |
|
|
try { |
|
|
const token = req.headers['authorization']?.replace('Bearer ', '') || req.cookies?.adminToken |
|
|
|
|
|
if (!token) { |
|
|
return res.status(401).json({ |
|
|
error: 'No token provided', |
|
|
message: 'Authentication required' |
|
|
}) |
|
|
} |
|
|
|
|
|
const sessionData = await redis.getSession(token) |
|
|
|
|
|
if (!sessionData) { |
|
|
return res.status(401).json({ |
|
|
error: 'Invalid token', |
|
|
message: 'Session expired or invalid' |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
sessionData.lastActivity = new Date().toISOString() |
|
|
await redis.setSession(token, sessionData, config.security.adminSessionTimeout) |
|
|
|
|
|
return res.json({ |
|
|
success: true, |
|
|
token, |
|
|
expiresIn: config.security.adminSessionTimeout |
|
|
}) |
|
|
} catch (error) { |
|
|
logger.error('❌ Token refresh error:', error) |
|
|
return res.status(500).json({ |
|
|
error: 'Token refresh failed', |
|
|
message: 'Internal server error' |
|
|
}) |
|
|
} |
|
|
}) |
|
|
|
|
|
module.exports = router |
|
|
|