Files
ManagerAccount/backend/server.js

1519 lines
60 KiB
JavaScript

// Backend Server for AccManager
// Express.js + mssql
const express = require('express');
const sql = require('mssql');
const cors = require('cors');
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const nodemailer = require('nodemailer');
const multer = require('multer');
const XLSX = require('xlsx');
const dotenv = require('dotenv');
const app = express();
dotenv.config();
function envBool(name, defaultValue) {
const value = process.env[name];
if (value === undefined) {
return defaultValue;
}
return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase());
}
const DB_SERVER = process.env.DB_SERVER || '172.20.235.176';
const DB_USER = process.env.DB_USER || 'sa';
const DB_PASSWORD = process.env.DB_PASSWORD || 'robotics@2022';
const DB_NAME = process.env.DB_NAME || 'AccManager';
const DB_ENCRYPT = envBool('DB_ENCRYPT', false);
const DB_TRUST_CERTIFICATE = envBool('DB_TRUST_CERTIFICATE', true);
const DB_CONNECT_TIMEOUT = Number(process.env.DB_CONNECT_TIMEOUT || 30000);
const BCRYPT_ROUNDS = Number(process.env.BCRYPT_ROUNDS || 12);
const PASSWORD_VIEW_SECRET = process.env.PASSWORD_VIEW_SECRET || 'change-this-password-view-secret';
const PASSWORD_VIEW_KEY = crypto.createHash('sha256').update(String(PASSWORD_VIEW_SECRET)).digest();
const PASSWORD_VIEW_PREFIX = 'enc:v1';
const PORT = process.env.PORT || 3000;
const APP_BASE_URL = String(process.env.APP_BASE_URL || `http://localhost:${PORT}`).replace(/\/+$/, '');
const SMTP_HOST = process.env.SMTP_HOST || '';
const SMTP_PORT = Number(process.env.SMTP_PORT || 587);
const SMTP_SECURE = envBool('SMTP_SECURE', false);
const SMTP_USER = process.env.SMTP_USER || '';
const SMTP_PASS = process.env.SMTP_PASS || '';
const SMTP_FROM = process.env.SMTP_FROM || SMTP_USER || 'no-reply@accmanager.local';
const EMAIL_VERIFY_TOKEN_TTL_MINUTES = Number(process.env.EMAIL_VERIFY_TOKEN_TTL_MINUTES || 30);
let mailTransporter;
function isBcryptHash(value) {
return typeof value === 'string' && /^\$2[aby]\$\d{2}\$[./A-Za-z0-9]{53}$/.test(value);
}
async function hashPassword(plainPassword) {
return bcrypt.hash(String(plainPassword), BCRYPT_ROUNDS);
}
async function verifyPassword(plainPassword, storedPassword) {
if (isBcryptHash(storedPassword)) {
return bcrypt.compare(String(plainPassword), storedPassword);
}
// Legacy fallback for old plain-text records.
return String(plainPassword) === String(storedPassword || '');
}
function encryptPasswordForView(plainPassword) {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', PASSWORD_VIEW_KEY, iv);
const encrypted = Buffer.concat([
cipher.update(String(plainPassword), 'utf8'),
cipher.final()
]);
const tag = cipher.getAuthTag();
return `${PASSWORD_VIEW_PREFIX}:${iv.toString('base64')}:${tag.toString('base64')}:${encrypted.toString('base64')}`;
}
function decryptPasswordForView(payload) {
try {
if (typeof payload !== 'string' || !payload.startsWith(`${PASSWORD_VIEW_PREFIX}:`)) {
return null;
}
const parts = payload.split(':');
if (parts.length !== 5) {
return null;
}
const iv = Buffer.from(parts[2], 'base64');
const tag = Buffer.from(parts[3], 'base64');
const encrypted = Buffer.from(parts[4], 'base64');
const decipher = crypto.createDecipheriv('aes-256-gcm', PASSWORD_VIEW_KEY, iv);
decipher.setAuthTag(tag);
const plain = Buffer.concat([decipher.update(encrypted), decipher.final()]);
return plain.toString('utf8');
} catch (err) {
return null;
}
}
function hashVerificationToken(token) {
return crypto.createHash('sha256').update(String(token)).digest('hex');
}
function generateEmailVerificationToken() {
const token = crypto.randomBytes(32).toString('hex');
return {
token,
tokenHash: hashVerificationToken(token)
};
}
function getEmailVerificationUrl(token) {
return `${APP_BASE_URL}/pages/verify-email.html?token=${encodeURIComponent(token)}`;
}
function canSendEmails() {
return Boolean(SMTP_HOST && SMTP_USER && SMTP_PASS);
}
function getMailTransporter() {
if (!mailTransporter) {
mailTransporter = nodemailer.createTransport({
host: SMTP_HOST,
port: SMTP_PORT,
secure: SMTP_SECURE,
auth: {
user: SMTP_USER,
pass: SMTP_PASS
}
});
}
return mailTransporter;
}
async function sendVerificationEmail({ email, username, token }) {
const verifyUrl = getEmailVerificationUrl(token);
if (!canSendEmails()) {
console.warn(`SMTP is not configured. Verification URL for ${email}: ${verifyUrl}`);
return {
sent: false,
previewUrl: verifyUrl,
reason: 'SMTP is not configured'
};
}
try {
const transporter = getMailTransporter();
await transporter.sendMail({
from: SMTP_FROM,
to: email,
subject: 'AccManager - Confirm your email',
text: `Hello ${username || 'there'},\n\nPlease confirm your email by opening this link:\n${verifyUrl}\n\nThis link will expire in ${EMAIL_VERIFY_TOKEN_TTL_MINUTES} minutes.\n\nIf you did not register, please ignore this email.`,
html: `
<div style="font-family: Arial, sans-serif; line-height: 1.5; color: #1f2937;">
<h2 style="margin-bottom: 8px;">Confirm your email</h2>
<p>Hello ${username || 'there'},</p>
<p>Thank you for registering. Please confirm your email by clicking the button below:</p>
<p style="margin: 18px 0;">
<a href="${verifyUrl}" style="background:#2563eb;color:#ffffff;padding:10px 16px;border-radius:8px;text-decoration:none;font-weight:700;display:inline-block;">Confirm Email</a>
</p>
<p>Or copy this URL into your browser:</p>
<p><a href="${verifyUrl}">${verifyUrl}</a></p>
<p>This link will expire in ${EMAIL_VERIFY_TOKEN_TTL_MINUTES} minutes.</p>
<p>If you did not register this account, you can ignore this message.</p>
</div>
`
});
return { sent: true };
} catch (err) {
console.error('Send verification email error:', err.message);
return {
sent: false,
reason: err.message
};
}
}
function getUserIdFromRequest(req) {
const rawUserId = req.headers['x-user-id'] || req.query.userId;
const userId = Number(rawUserId);
return Number.isInteger(userId) && userId > 0 ? userId : null;
}
// Middleware
app.use(cors());
app.use(express.json());
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 15 * 1024 * 1024 }
});
// Serve static files from /public
const path = require('path');
const publicDir = path.join(__dirname, '..', 'public');
app.use(express.static(publicDir));
// Root route
app.get('/', (req, res) => {
res.sendFile(path.join(publicDir, 'pages', 'login.html'));
});
// SQL Server Configuration
const sqlConfig = {
server: DB_SERVER,
authentication: {
type: 'default',
options: {
userName: DB_USER,
password: DB_PASSWORD
}
},
options: {
database: DB_NAME,
trustServerCertificate: DB_TRUST_CERTIFICATE,
enableKeepAlive: true,
connectTimeout: DB_CONNECT_TIMEOUT,
encrypt: DB_ENCRYPT
}
};
// Initialize Database Pool
let pool;
async function initializeDatabase() {
try {
pool = new sql.ConnectionPool(sqlConfig);
await pool.connect();
console.log('✓ Connected to SQL Server');
// Check and create database if not exists
const masterConnection = new sql.ConnectionPool({
server: DB_SERVER,
authentication: { type: 'default', options: { userName: DB_USER, password: DB_PASSWORD } },
options: {
connectTimeout: DB_CONNECT_TIMEOUT,
database: 'master',
trustServerCertificate: DB_TRUST_CERTIFICATE,
encrypt: DB_ENCRYPT
}
});
await masterConnection.connect();
const createDbResult = await masterConnection.request()
.query(`IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = 'AccManager')
BEGIN
CREATE DATABASE AccManager;
END`);
await masterConnection.close();
// Now create tables in AccManager
await createTables();
await migrateLegacyPasswords();
console.log('✓ Database and tables created');
} catch (err) {
console.error('Database connection failed:', err);
process.exit(1);
}
}
async function migrateLegacyPasswords() {
try {
const usersResult = await pool.request()
.query('SELECT UserId, Password, ViewPassword FROM Users WHERE Password IS NOT NULL');
let migratedCount = 0;
for (const row of usersResult.recordset) {
const request = pool.request()
.input('userId', sql.Int, row.UserId);
let hasUpdates = false;
const rawPassword = String(row.Password || '');
if (!row.ViewPassword && !isBcryptHash(rawPassword)) {
request.input('viewPassword', sql.NVarChar, encryptPasswordForView(rawPassword));
hasUpdates = true;
}
if (!isBcryptHash(row.Password)) {
const hashedPassword = await hashPassword(row.Password);
request.input('password', sql.NVarChar, hashedPassword);
hasUpdates = true;
migratedCount += 1;
}
if (hasUpdates) {
await request.query(`UPDATE Users
SET ${!isBcryptHash(rawPassword) ? 'Password = @password' : 'Password = Password'}
${(!row.ViewPassword && !isBcryptHash(rawPassword)) ? ', ViewPassword = @viewPassword' : ''}
WHERE UserId = @userId`);
}
}
if (migratedCount > 0) {
console.log(`✓ Migrated ${migratedCount} legacy plain-text password(s) to bcrypt`);
}
} catch (err) {
console.error('Password migration error:', err.message);
}
}
async function createTables() {
const queries = [
// Users Table
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Users')
BEGIN
CREATE TABLE Users (
UserId INT PRIMARY KEY IDENTITY(1,1),
Username NVARCHAR(50) UNIQUE NOT NULL,
Password NVARCHAR(255) NOT NULL,
Email NVARCHAR(100),
FullName NVARCHAR(100),
Role NVARCHAR(50) NOT NULL,
Status NVARCHAR(20) DEFAULT 'Active',
CreatedDate DATETIME DEFAULT GETDATE(),
LastLogin DATETIME,
IsActive BIT DEFAULT 1
)
END`,
// Applications Table
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Applications')
BEGIN
CREATE TABLE Applications (
AppId INT PRIMARY KEY IDENTITY(1,1),
Name NVARCHAR(100) NOT NULL,
Type NVARCHAR(50),
Status NVARCHAR(20) DEFAULT 'online',
Icon NVARCHAR(50),
Description NVARCHAR(500),
Url NVARCHAR(255),
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE()
)
END`,
// Accounts Table
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Accounts')
BEGIN
CREATE TABLE Accounts (
AccountId INT PRIMARY KEY IDENTITY(1,1),
UserId INT NOT NULL,
AppId INT NOT NULL,
AccountUsername NVARCHAR(100),
AccountPassword NVARCHAR(255),
Email NVARCHAR(100),
AccessLevel NVARCHAR(50),
Status NVARCHAR(20) DEFAULT 'Active',
Notes NVARCHAR(MAX),
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE(),
FOREIGN KEY (UserId) REFERENCES Users(UserId) ON DELETE CASCADE,
FOREIGN KEY (AppId) REFERENCES Applications(AppId) ON DELETE CASCADE
)
END`,
// AuditLog Table
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AuditLog')
BEGIN
CREATE TABLE AuditLog (
LogId INT PRIMARY KEY IDENTITY(1,1),
UserId INT,
Action NVARCHAR(50),
TableName NVARCHAR(50),
RecordId INT,
OldValue NVARCHAR(MAX),
NewValue NVARCHAR(MAX),
Timestamp DATETIME DEFAULT GETDATE(),
FOREIGN KEY (UserId) REFERENCES Users(UserId)
)
END`
];
for (let query of queries) {
try {
await pool.request().query(query);
} catch (err) {
console.error('Table creation error:', err.message);
}
}
// Ensure AssetInventory indexes exist for lookup/filter performance
try {
await pool.request().query(`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetInventory_AssetCode') CREATE INDEX IX_AssetInventory_AssetCode ON AssetInventory(AssetCode);`);
await pool.request().query(`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetInventory_Status') CREATE INDEX IX_AssetInventory_Status ON AssetInventory(Status);`);
} catch (err) {
console.error('AssetInventory index creation error:', err.message);
}
// Ensure new columns exist on Applications for migrations
try {
await pool.request().query(`IF COL_LENGTH('dbo.Applications','Url') IS NULL ALTER TABLE Applications ADD Url NVARCHAR(255);`);
await pool.request().query(`IF COL_LENGTH('dbo.Applications','Description') IS NULL ALTER TABLE Applications ADD Description NVARCHAR(500);`);
await pool.request().query(`IF COL_LENGTH('dbo.Users','ViewPassword') IS NULL ALTER TABLE Users ADD ViewPassword NVARCHAR(1024);`);
await pool.request().query(`IF COL_LENGTH('dbo.Users','EmailVerified') IS NULL ALTER TABLE Users ADD EmailVerified BIT NOT NULL CONSTRAINT DF_Users_EmailVerified DEFAULT(0);`);
await pool.request().query(`IF COL_LENGTH('dbo.Users','EmailVerifiedAt') IS NULL ALTER TABLE Users ADD EmailVerifiedAt DATETIME NULL;`);
await pool.request().query(`IF COL_LENGTH('dbo.Users','EmailVerifyToken') IS NULL ALTER TABLE Users ADD EmailVerifyToken NVARCHAR(255) NULL;`);
await pool.request().query(`IF COL_LENGTH('dbo.Users','EmailVerifyTokenExpires') IS NULL ALTER TABLE Users ADD EmailVerifyTokenExpires DATETIME NULL;`);
await pool.request().query(`UPDATE Users SET EmailVerified = 1, EmailVerifiedAt = ISNULL(EmailVerifiedAt, GETDATE()) WHERE LOWER(ISNULL(Role, '')) = 'admin';`);
// Backfill Url to empty string to avoid undefined in responses
await pool.request().query(`UPDATE Applications SET Url = '' WHERE Url IS NULL;`);
} catch (err) {
console.error('Column addition error (Applications):', err.message);
}
// Insert initial admin user
try {
const adminPasswordHash = await hashPassword('admin');
const adminViewPassword = encryptPasswordForView('admin');
await pool.request()
.input('username', sql.NVarChar, 'admin')
.input('password', sql.NVarChar, adminPasswordHash)
.input('viewPassword', sql.NVarChar, adminViewPassword)
.input('email', sql.NVarChar, 'admin@accmanager.local')
.input('fullname', sql.NVarChar, 'Administrator')
.input('role', sql.NVarChar, 'admin')
.query(`IF NOT EXISTS (SELECT * FROM Users WHERE Username = @username)
INSERT INTO Users (Username, Password, ViewPassword, Email, FullName, Role, IsActive, EmailVerified, EmailVerifiedAt)
VALUES (@username, @password, @viewPassword, @email, @fullname, @role, 1, 1, GETDATE())
ELSE
UPDATE Users
SET EmailVerified = 1,
EmailVerifiedAt = ISNULL(EmailVerifiedAt, GETDATE())
WHERE Username = @username`);
console.log('✓ Admin user created: admin / admin');
} catch (err) {
console.error('Admin user error:', err.message);
}
// Insert sample applications
try {
await pool.request()
.query(`IF (SELECT COUNT(*) FROM Applications) = 0
BEGIN
INSERT INTO Applications (Name, Type, Status, Icon, Description, Url)
VALUES
('AWS', 'Cloud', 'online', 'cloud', 'Amazon Web Services', 'https://aws.amazon.com'),
('GitHub', 'VCS', 'online', 'code', 'GitHub - Version Control', 'https://github.com'),
('Google Workspace', 'Collaboration', 'online', 'mail', 'Google Workspace', 'https://workspace.google.com'),
('Nginx Proxy', 'Infra', 'offline', 'dns', 'Nginx Web Server', 'https://nginx.org')
END`);
console.log('✓ Sample applications created');
} catch (err) {
console.error('Applications error:', err.message);
}
}
// ==========================================
// API ROUTES - Authentication
// ==========================================
// Login endpoint
app.post('/api/auth/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({
success: false,
message: 'Username and password are required'
});
}
const result = await pool.request()
.input('username', sql.NVarChar, username)
.query(`SELECT UserId, Username, Email, FullName, Role, RoleId, Status, Password, EmailVerified
FROM Users
WHERE (Username = @username OR Email = @username)
AND IsActive = 1`);
if (result.recordset.length > 0) {
const dbUser = result.recordset[0];
const isValidPassword = await verifyPassword(password, dbUser.Password);
if (!isValidPassword) {
return res.status(401).json({
success: false,
message: 'Invalid username or password'
});
}
if (!dbUser.EmailVerified) {
return res.status(403).json({
success: false,
message: 'Please confirm your email before signing in',
requiresEmailVerification: true,
email: dbUser.Email,
username: dbUser.Username
});
}
// Upgrade old plain-text passwords after successful legacy login.
if (!isBcryptHash(dbUser.Password)) {
const upgradedHash = await hashPassword(password);
await pool.request()
.input('userId', sql.Int, dbUser.UserId)
.input('password', sql.NVarChar, upgradedHash)
.input('viewPassword', sql.NVarChar, encryptPasswordForView(password))
.query('UPDATE Users SET Password = @password, ViewPassword = ISNULL(ViewPassword, @viewPassword) WHERE UserId = @userId');
}
const { Password: _, ...safeUser } = dbUser;
const user = { ...safeUser, role: safeUser.Role || safeUser.role || 'guest' };
// Update last login
await pool.request()
.input('userId', sql.Int, user.UserId)
.query('UPDATE Users SET LastLogin = GETDATE() WHERE UserId = @userId');
res.json({
success: true,
message: 'Login successful',
user: user
});
} else {
res.status(401).json({
success: false,
message: 'Invalid username or password'
});
}
} catch (err) {
console.error('Login error:', err);
res.status(500).json({ success: false, message: err.message });
}
});
// Public registration endpoint
app.post('/api/auth/register', async (req, res) => {
try {
const { username, password, email, fullname } = req.body;
if (!username || !password || !email) {
return res.status(400).json({ success: false, message: 'Username, password and email are required' });
}
const normalizedEmail = String(email).trim().toLowerCase();
const safeUsername = String(username).trim();
const isEmailFormatValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail);
if (!isEmailFormatValid) {
return res.status(400).json({ success: false, message: 'Email format is invalid' });
}
// Prevent duplicate usernames/emails
const existing = await pool.request()
.input('username', sql.NVarChar, safeUsername)
.input('email', sql.NVarChar, normalizedEmail)
.query('SELECT TOP 1 UserId, Username, Email FROM Users WHERE Username = @username OR Email = @email');
if (existing.recordset.length > 0) {
const existed = existing.recordset[0];
const duplicateByEmail = String(existed.Email || '').toLowerCase() === normalizedEmail;
return res.status(409).json({
success: false,
message: duplicateByEmail ? 'Email already exists' : 'Username already exists'
});
}
const hashedPassword = await hashPassword(password);
const viewPassword = encryptPasswordForView(password);
const { token, tokenHash } = generateEmailVerificationToken();
const safeFullname = fullname && fullname.trim() ? fullname.trim() : safeUsername;
let guestRoleName = 'guest';
let guestRoleId = null;
const hasRoleIdColResult = await pool.request()
.query("SELECT CASE WHEN COL_LENGTH('dbo.Users','RoleId') IS NULL THEN 0 ELSE 1 END AS HasRoleId");
const hasRoleIdColumn = hasRoleIdColResult.recordset[0].HasRoleId === 1;
const hasRolesTableResult = await pool.request()
.query("SELECT CASE WHEN OBJECT_ID('dbo.Roles','U') IS NULL THEN 0 ELSE 1 END AS HasRolesTable");
const hasRolesTable = hasRolesTableResult.recordset[0].HasRolesTable === 1;
if (hasRolesTable) {
const guestRoleResult = await pool.request().query(`
IF NOT EXISTS (SELECT 1 FROM Roles WHERE LOWER(RoleName) = 'guest')
BEGIN
INSERT INTO Roles (RoleName, Description)
VALUES ('Guest', 'Default role for self-registered users');
END
SELECT TOP 1 RoleId, RoleName
FROM Roles
WHERE LOWER(RoleName) = 'guest';
`);
if (guestRoleResult.recordset.length > 0) {
guestRoleId = guestRoleResult.recordset[0].RoleId;
guestRoleName = guestRoleResult.recordset[0].RoleName || 'guest';
}
}
let result;
if (hasRoleIdColumn && guestRoleId !== null) {
result = await pool.request()
.input('username', sql.NVarChar, safeUsername)
.input('password', sql.NVarChar, hashedPassword)
.input('email', sql.NVarChar, normalizedEmail)
.input('fullname', sql.NVarChar, safeFullname)
.input('roleId', sql.Int, guestRoleId)
.input('role', sql.NVarChar, guestRoleName)
.input('viewPassword', sql.NVarChar, viewPassword)
.input('emailVerifyToken', sql.NVarChar, tokenHash)
.input('tokenTtlMinutes', sql.Int, EMAIL_VERIFY_TOKEN_TTL_MINUTES)
.query(`INSERT INTO Users (Username, Password, ViewPassword, Email, FullName, RoleId, Role, Status, IsActive, EmailVerified, EmailVerifyToken, EmailVerifyTokenExpires)
OUTPUT INSERTED.UserId, INSERTED.Username, INSERTED.Email, INSERTED.FullName, INSERTED.Role, INSERTED.RoleId
VALUES (@username, @password, @viewPassword, @email, @fullname, @roleId, @role, 'Active', 1, 0, @emailVerifyToken, DATEADD(MINUTE, @tokenTtlMinutes, GETDATE()))`);
} else {
result = await pool.request()
.input('username', sql.NVarChar, safeUsername)
.input('password', sql.NVarChar, hashedPassword)
.input('email', sql.NVarChar, normalizedEmail)
.input('fullname', sql.NVarChar, safeFullname)
.input('role', sql.NVarChar, guestRoleName)
.input('viewPassword', sql.NVarChar, viewPassword)
.input('emailVerifyToken', sql.NVarChar, tokenHash)
.input('tokenTtlMinutes', sql.Int, EMAIL_VERIFY_TOKEN_TTL_MINUTES)
.query(`INSERT INTO Users (Username, Password, ViewPassword, Email, FullName, Role, Status, IsActive, EmailVerified, EmailVerifyToken, EmailVerifyTokenExpires)
OUTPUT INSERTED.UserId, INSERTED.Username, INSERTED.Email, INSERTED.FullName, INSERTED.Role
VALUES (@username, @password, @viewPassword, @email, @fullname, @role, 'Active', 1, 0, @emailVerifyToken, DATEADD(MINUTE, @tokenTtlMinutes, GETDATE()))`);
}
const inserted = result.recordset[0];
// Repair previous self-registered users that were wrongly assigned Admin RoleId.
if (hasRoleIdColumn && guestRoleId !== null) {
await pool.request()
.input('guestRoleId', sql.Int, guestRoleId)
.query(`UPDATE Users
SET RoleId = @guestRoleId,
Role = 'Guest'
WHERE LOWER(Role) = 'guest'
AND (RoleId IS NULL OR RoleId <> @guestRoleId)`);
}
const emailResult = await sendVerificationEmail({
email: normalizedEmail,
username: safeUsername,
token
});
const responsePayload = {
success: true,
message: emailResult.sent
? 'Registration successful. Please check your email to confirm your account.'
: 'Registration successful, but email sending is not configured. Please contact administrator or use verification preview link in development.',
registrationPendingVerification: true,
emailSent: emailResult.sent,
userId: inserted?.UserId
};
if (emailResult.previewUrl) {
responsePayload.verificationPreviewUrl = emailResult.previewUrl;
}
if (emailResult.reason && !emailResult.sent) {
responsePayload.emailError = emailResult.reason;
}
res.json(responsePayload);
} catch (err) {
console.error('Registration error:', err);
res.status(500).json({ success: false, message: 'Registration failed' });
}
});
app.get('/api/auth/verify-email', async (req, res) => {
try {
const token = String(req.query.token || '').trim();
if (!token) {
return res.status(400).json({ success: false, message: 'Verification token is required' });
}
const tokenHash = hashVerificationToken(token);
const result = await pool.request()
.input('token', sql.NVarChar, tokenHash)
.query(`SELECT UserId, Username, Email, FullName, Role, RoleId, Status, IsActive, EmailVerifyTokenExpires
FROM Users
WHERE EmailVerifyToken = @token`);
if (result.recordset.length === 0) {
return res.status(400).json({ success: false, message: 'Verification token is invalid or already used' });
}
const verifiedUser = result.recordset[0];
const expiresAt = verifiedUser.EmailVerifyTokenExpires ? new Date(verifiedUser.EmailVerifyTokenExpires) : null;
if (!expiresAt || expiresAt.getTime() < Date.now()) {
return res.status(400).json({ success: false, message: 'Verification token has expired. Please request a new email.' });
}
await pool.request()
.input('userId', sql.Int, verifiedUser.UserId)
.query(`UPDATE Users
SET EmailVerified = 1,
EmailVerifiedAt = GETDATE(),
EmailVerifyToken = NULL,
EmailVerifyTokenExpires = NULL
WHERE UserId = @userId`);
if (!verifiedUser.IsActive) {
return res.status(403).json({
success: false,
message: 'Email confirmed, but account is inactive. Please contact administrator.'
});
}
// Align verification flow with login payload shape to support auto-login on verify page.
const { EmailVerifyTokenExpires: _expires, ...safeUser } = verifiedUser;
const user = { ...safeUser, role: safeUser.Role || safeUser.role || 'guest' };
await pool.request()
.input('userId', sql.Int, verifiedUser.UserId)
.query('UPDATE Users SET LastLogin = GETDATE() WHERE UserId = @userId');
res.json({
success: true,
message: 'Email confirmed successfully. Logging you in...',
autoLogin: true,
user
});
} catch (err) {
console.error('Verify email error:', err.message);
res.status(500).json({ success: false, message: 'Email verification failed' });
}
});
app.post('/api/auth/resend-verification', async (req, res) => {
try {
const identifier = String(req.body?.identifier || req.body?.email || '').trim();
if (!identifier) {
return res.status(400).json({ success: false, message: 'Username or email is required' });
}
const result = await pool.request()
.input('identifier', sql.NVarChar, identifier)
.query(`SELECT TOP 1 UserId, Username, Email, EmailVerified
FROM Users
WHERE Username = @identifier OR Email = @identifier`);
if (result.recordset.length === 0) {
return res.json({ success: true, message: 'If the account exists, a confirmation email has been sent.' });
}
const user = result.recordset[0];
if (user.EmailVerified) {
return res.json({ success: true, message: 'This email is already confirmed.' });
}
if (!user.Email) {
return res.status(400).json({ success: false, message: 'This account has no email. Please contact administrator.' });
}
const { token, tokenHash } = generateEmailVerificationToken();
await pool.request()
.input('userId', sql.Int, user.UserId)
.input('tokenHash', sql.NVarChar, tokenHash)
.input('tokenTtlMinutes', sql.Int, EMAIL_VERIFY_TOKEN_TTL_MINUTES)
.query(`UPDATE Users
SET EmailVerifyToken = @tokenHash,
EmailVerifyTokenExpires = DATEADD(MINUTE, @tokenTtlMinutes, GETDATE())
WHERE UserId = @userId`);
const emailResult = await sendVerificationEmail({
email: user.Email,
username: user.Username,
token
});
const payload = {
success: true,
message: emailResult.sent
? 'Verification email sent. Please check your inbox.'
: 'SMTP is not configured. Use development preview link or configure SMTP.',
emailSent: emailResult.sent
};
if (emailResult.previewUrl) {
payload.verificationPreviewUrl = emailResult.previewUrl;
}
if (emailResult.reason && !emailResult.sent) {
payload.emailError = emailResult.reason;
}
res.json(payload);
} catch (err) {
console.error('Resend verification error:', err.message);
res.status(500).json({ success: false, message: 'Cannot resend verification email right now' });
}
});
// Middleware for role-based access control
function normalizeRole(value) {
return String(value || '').trim().toLowerCase();
}
function requireRoles(roles = [], message = 'Access denied') {
const allowedRoles = new Set(roles.map(normalizeRole));
return (req, res, next) => {
const userRole = normalizeRole(req.headers['x-user-role'] || req.query.userRole);
if (!allowedRoles.has(userRole)) {
return res.status(403).json({ success: false, message });
}
next();
};
}
const requireAdmin = requireRoles(['admin'], 'Admin access required');
const requireAssetOrAdmin = requireRoles(['asset', 'admin'], 'Asset or Admin access required');
// ==========================================
// API ROUTES - Roles
// ==========================================
// Get all roles
app.get('/api/roles', async (req, res) => {
try {
const result = await pool.request()
.query('SELECT RoleId, RoleName, Description, CreatedDate FROM Roles ORDER BY RoleName');
res.json({ success: true, data: result.recordset });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Get role by ID
app.get('/api/roles/:id', async (req, res) => {
try {
const result = await pool.request()
.input('roleId', sql.Int, req.params.id)
.query('SELECT * FROM Roles WHERE RoleId = @roleId');
if (result.recordset.length > 0) {
res.json({ success: true, data: result.recordset[0] });
} else {
res.status(404).json({ success: false, message: 'Role not found' });
}
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Create new role (Admin only)
app.post('/api/roles', requireAdmin, async (req, res) => {
try {
const { roleName, description } = req.body;
const result = await pool.request()
.input('roleName', sql.NVarChar, roleName)
.input('description', sql.NVarChar, description)
.query(`IF NOT EXISTS (SELECT * FROM Roles WHERE RoleName = @roleName)
BEGIN
INSERT INTO Roles (RoleName, Description)
VALUES (@roleName, @description);
SELECT SCOPE_IDENTITY() as RoleId
END
ELSE
BEGIN
SELECT RoleId FROM Roles WHERE RoleName = @roleName
END`);
res.json({ success: true, message: 'Role created', roleId: result.recordset[0].RoleId });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Update role (Admin only)
app.put('/api/roles/:id', requireAdmin, async (req, res) => {
try {
const { roleName, description } = req.body;
await pool.request()
.input('roleId', sql.Int, req.params.id)
.input('roleName', sql.NVarChar, roleName)
.input('description', sql.NVarChar, description)
.query(`UPDATE Roles
SET RoleName = @roleName,
Description = @description
WHERE RoleId = @roleId`);
res.json({ success: true, message: 'Role updated' });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Delete role (Admin only)
app.delete('/api/roles/:id', requireAdmin, async (req, res) => {
try {
// Check if role is in use
const check = await pool.request()
.input('roleId', sql.Int, req.params.id)
.query('SELECT COUNT(*) as Count FROM Users WHERE RoleId = @roleId');
if (check.recordset[0].Count > 0) {
return res.status(400).json({
success: false,
message: 'Cannot delete role - it is assigned to users'
});
}
await pool.request()
.input('roleId', sql.Int, req.params.id)
.query('DELETE FROM Roles WHERE RoleId = @roleId');
res.json({ success: true, message: 'Role deleted' });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// ==========================================
// API ROUTES - Users
// ==========================================
// Get all users
app.get('/api/users', async (req, res) => {
try {
const result = await pool.request()
.query(`SELECT u.UserId, u.Username, u.Email, u.FullName, u.Role, u.RoleId, r.RoleName,
u.Status, u.CreatedDate, u.LastLogin, u.IsActive, u.EmailVerified, u.EmailVerifiedAt
FROM Users u
LEFT JOIN Roles r ON u.RoleId = r.RoleId
ORDER BY u.CreatedDate DESC`);
res.json({ success: true, data: result.recordset });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
app.get('/api/users/me', async (req, res) => {
try {
const userId = getUserIdFromRequest(req);
if (!userId) {
return res.status(401).json({ success: false, message: 'Missing or invalid user id' });
}
const result = await pool.request()
.input('userId', sql.Int, userId)
.query(`SELECT UserId, Username, Email, FullName, Role, RoleId, Status, IsActive,
CreatedDate, LastLogin, EmailVerified, EmailVerifiedAt
FROM Users
WHERE UserId = @userId`);
if (result.recordset.length === 0) {
return res.status(404).json({ success: false, message: 'User not found' });
}
res.json({ success: true, data: result.recordset[0] });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
app.put('/api/users/me', async (req, res) => {
try {
const userId = getUserIdFromRequest(req);
if (!userId) {
return res.status(401).json({ success: false, message: 'Missing or invalid user id' });
}
const fullname = String(req.body?.fullname || '').trim();
const incomingEmail = String(req.body?.email || '').trim().toLowerCase();
const currentPassword = String(req.body?.currentPassword || '');
const newPassword = String(req.body?.newPassword || '').trim();
if (!fullname) {
return res.status(400).json({ success: false, message: 'Full name is required' });
}
if (!incomingEmail) {
return res.status(400).json({ success: false, message: 'Email is required' });
}
const isEmailFormatValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(incomingEmail);
if (!isEmailFormatValid) {
return res.status(400).json({ success: false, message: 'Email format is invalid' });
}
const userResult = await pool.request()
.input('userId', sql.Int, userId)
.query(`SELECT UserId, Username, Email, Password, Role, RoleId
FROM Users
WHERE UserId = @userId`);
if (userResult.recordset.length === 0) {
return res.status(404).json({ success: false, message: 'User not found' });
}
const existingUser = userResult.recordset[0];
const emailChanged = String(existingUser.Email || '').toLowerCase() !== incomingEmail;
const shouldChangePassword = newPassword.length > 0;
if (shouldChangePassword) {
if (!currentPassword) {
return res.status(400).json({ success: false, message: 'Current password is required to set a new password' });
}
const isCurrentPasswordValid = await verifyPassword(currentPassword, existingUser.Password);
if (!isCurrentPasswordValid) {
return res.status(400).json({ success: false, message: 'Current password is incorrect' });
}
}
if (emailChanged) {
const duplicateEmailResult = await pool.request()
.input('email', sql.NVarChar, incomingEmail)
.input('userId', sql.Int, userId)
.query('SELECT TOP 1 UserId FROM Users WHERE Email = @email AND UserId <> @userId');
if (duplicateEmailResult.recordset.length > 0) {
return res.status(409).json({ success: false, message: 'Email already exists' });
}
}
const request = pool.request()
.input('userId', sql.Int, userId)
.input('fullname', sql.NVarChar, fullname)
.input('email', sql.NVarChar, incomingEmail);
let token;
if (emailChanged) {
const tokenResult = generateEmailVerificationToken();
token = tokenResult.token;
request.input('emailVerifyToken', sql.NVarChar, tokenResult.tokenHash);
request.input('tokenTtlMinutes', sql.Int, EMAIL_VERIFY_TOKEN_TTL_MINUTES);
}
if (shouldChangePassword) {
const hashedPassword = await hashPassword(newPassword);
const viewPassword = encryptPasswordForView(newPassword);
request.input('password', sql.NVarChar, hashedPassword);
request.input('viewPassword', sql.NVarChar, viewPassword);
}
await request.query(`UPDATE Users
SET FullName = @fullname,
Email = @email
${shouldChangePassword ? ', Password = @password, ViewPassword = @viewPassword' : ''}
${emailChanged ? ', EmailVerified = 0, EmailVerifiedAt = NULL, EmailVerifyToken = @emailVerifyToken, EmailVerifyTokenExpires = DATEADD(MINUTE, @tokenTtlMinutes, GETDATE())' : ''}
WHERE UserId = @userId`);
let emailResult = { sent: true };
if (emailChanged) {
emailResult = await sendVerificationEmail({
email: incomingEmail,
username: existingUser.Username,
token
});
}
const safeResult = await pool.request()
.input('userId', sql.Int, userId)
.query(`SELECT UserId, Username, Email, FullName, Role, RoleId, Status, IsActive,
CreatedDate, LastLogin, EmailVerified, EmailVerifiedAt
FROM Users
WHERE UserId = @userId`);
const payload = {
success: true,
message: emailChanged
? 'Profile updated. Please confirm your new email address.'
: (shouldChangePassword ? 'Profile and password updated successfully' : 'Profile updated successfully'),
user: safeResult.recordset[0],
verificationRequired: emailChanged,
emailSent: emailChanged ? emailResult.sent : undefined
};
if (emailChanged && emailResult.previewUrl) {
payload.verificationPreviewUrl = emailResult.previewUrl;
}
if (emailChanged && emailResult.reason && !emailResult.sent) {
payload.emailError = emailResult.reason;
}
res.json(payload);
} catch (err) {
console.error('Update profile error:', err.message);
res.status(500).json({ success: false, message: 'Cannot update profile right now' });
}
});
// Get user by ID
app.get('/api/users/:id', requireAdmin, async (req, res) => {
try {
const result = await pool.request()
.input('userId', sql.Int, req.params.id)
.query(`SELECT u.*, r.RoleName FROM Users u
LEFT JOIN Roles r ON u.RoleId = r.RoleId
WHERE u.UserId = @userId`);
if (result.recordset.length > 0) {
const record = result.recordset[0];
const viewPassword = decryptPasswordForView(record.ViewPassword || '');
const fallbackPlainPassword = !isBcryptHash(record.Password) ? String(record.Password || '') : '';
const plainPassword = viewPassword || fallbackPlainPassword;
const userDetails = {
...record,
Password: plainPassword,
PasswordAvailable: Boolean(plainPassword)
};
res.json({ success: true, data: userDetails });
} else {
res.status(404).json({ success: false, message: 'User not found' });
}
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Create new user (Admin only)
app.post('/api/users', requireAdmin, async (req, res) => {
try {
const { username, password, email, fullname, roleId } = req.body;
if (!username || !password) {
return res.status(400).json({ success: false, message: 'Username and password are required' });
}
const hashedPassword = await hashPassword(password);
const viewPassword = encryptPasswordForView(password);
const finalRoleId = roleId || 2; // Default to Guest role
// Get role name from Roles table
const roleResult = await pool.request()
.input('roleId', sql.Int, finalRoleId)
.query('SELECT RoleName FROM Roles WHERE RoleId = @roleId');
const roleName = roleResult.recordset.length > 0 ? roleResult.recordset[0].RoleName : 'guest';
const result = await pool.request()
.input('username', sql.NVarChar, username)
.input('password', sql.NVarChar, hashedPassword)
.input('email', sql.NVarChar, email || null)
.input('fullname', sql.NVarChar, fullname)
.input('roleId', sql.Int, finalRoleId)
.input('role', sql.NVarChar, roleName)
.input('viewPassword', sql.NVarChar, viewPassword)
.query(`IF NOT EXISTS (SELECT * FROM Users WHERE Username = @username)
BEGIN
INSERT INTO Users (Username, Password, ViewPassword, Email, FullName, RoleId, Role, IsActive)
VALUES (@username, @password, @viewPassword, @email, @fullname, @roleId, @role, 1);
SELECT SCOPE_IDENTITY() as UserId
END
ELSE
BEGIN
SELECT NULL as UserId
END`);
if (result.recordset[0].UserId) {
res.json({ success: true, message: 'User created', userId: result.recordset[0].UserId });
} else {
res.status(400).json({ success: false, message: 'Username already exists' });
}
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Update user (Admin only)
app.put('/api/users/:id', requireAdmin, async (req, res) => {
try {
const { email, fullname, roleId, status, isActive, password } = req.body;
const nextPassword = typeof password === 'string' ? password.trim() : '';
const shouldUpdatePassword = nextPassword.length > 0;
// Get role name from Roles table
let roleName = '';
if (roleId) {
const roleResult = await pool.request()
.input('roleId', sql.Int, roleId)
.query('SELECT RoleName FROM Roles WHERE RoleId = @roleId');
roleName = roleResult.recordset.length > 0 ? roleResult.recordset[0].RoleName : '';
}
const request = pool.request()
.input('userId', sql.Int, req.params.id)
.input('email', sql.NVarChar, email || null)
.input('fullname', sql.NVarChar, fullname)
.input('roleId', sql.Int, roleId)
.input('role', sql.NVarChar, roleName)
.input('status', sql.NVarChar, status)
.input('isActive', sql.Bit, isActive);
if (shouldUpdatePassword) {
const hashedPassword = await hashPassword(nextPassword);
const viewPassword = encryptPasswordForView(nextPassword);
request.input('password', sql.NVarChar, hashedPassword);
request.input('viewPassword', sql.NVarChar, viewPassword);
}
await request.query(`UPDATE Users
SET Email = @email,
FullName = @fullname,
RoleId = @roleId,
Role = @role,
Status = @status,
IsActive = @isActive
${shouldUpdatePassword ? ', Password = @password, ViewPassword = @viewPassword' : ''}
WHERE UserId = @userId`);
res.json({ success: true, message: shouldUpdatePassword ? 'User updated and password changed' : 'User updated' });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Delete user (Admin only)
app.delete('/api/users/:id', requireAdmin, async (req, res) => {
try {
// Prevent deleting the current admin user (ID = 1)
if (req.params.id === '1') {
return res.status(400).json({
success: false,
message: 'Cannot delete the primary admin user'
});
}
// Delete associated accounts first
await pool.request()
.input('userId', sql.Int, req.params.id)
.query('DELETE FROM Accounts WHERE UserId = @userId');
// Then delete the user
await pool.request()
.input('userId', sql.Int, req.params.id)
.query('DELETE FROM Users WHERE UserId = @userId');
res.json({ success: true, message: 'User deleted' });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// ==========================================
// API ROUTES - Applications
// ==========================================
// Get all applications
app.get('/api/applications', async (req, res) => {
try {
const result = await pool.request()
.query('SELECT AppId, Name, Type, Status, Icon, Description, Url, CreatedDate, UpdatedDate FROM Applications ORDER BY Name');
res.json({ success: true, data: result.recordset });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Create application
app.post('/api/applications', async (req, res) => {
try {
const { name, type, status, icon, description, url } = req.body;
const result = await pool.request()
.input('name', sql.NVarChar, name)
.input('type', sql.NVarChar, type)
.input('status', sql.NVarChar, status)
.input('icon', sql.NVarChar, icon)
.input('description', sql.NVarChar, description)
.input('url', sql.NVarChar, url)
.query(`INSERT INTO Applications (Name, Type, Status, Icon, Description, Url)
VALUES (@name, @type, @status, @icon, @description, @url);
SELECT SCOPE_IDENTITY() as AppId`);
res.json({ success: true, message: 'Application created', appId: result.recordset[0].AppId });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Update application
app.put('/api/applications/:id', async (req, res) => {
try {
const { name, type, status, icon, description, url } = req.body;
await pool.request()
.input('appId', sql.Int, req.params.id)
.input('name', sql.NVarChar, name)
.input('type', sql.NVarChar, type)
.input('status', sql.NVarChar, status)
.input('icon', sql.NVarChar, icon)
.input('description', sql.NVarChar, description)
.input('url', sql.NVarChar, url)
.query(`UPDATE Applications
SET Name = @name,
Type = @type,
Status = @status,
Icon = @icon,
Description = @description,
Url = @url,
UpdatedDate = GETDATE()
WHERE AppId = @appId`);
res.json({ success: true, message: 'Application updated' });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Delete application
app.delete('/api/applications/:id', async (req, res) => {
try {
await pool.request()
.input('appId', sql.Int, req.params.id)
.query('DELETE FROM Applications WHERE AppId = @appId');
res.json({ success: true, message: 'Application deleted' });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// ==========================================
// API ROUTES - Accounts
// ==========================================
// Get accounts for a user
app.get('/api/accounts/user/:userId', async (req, res) => {
try {
const result = await pool.request()
.input('userId', sql.Int, req.params.userId)
.query(`SELECT a.*, app.Name as AppName, app.Type as AppType, u.Username
FROM Accounts a
JOIN Applications app ON a.AppId = app.AppId
JOIN Users u ON a.UserId = u.UserId
WHERE a.UserId = @userId
ORDER BY a.CreatedDate DESC`);
res.json({ success: true, data: result.recordset });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Get all accounts (from all users)
app.get('/api/accounts/all', async (req, res) => {
try {
const result = await pool.request()
.query(`SELECT a.*, app.Name as AppName, app.Type as AppType, u.Username, u.FullName
FROM Accounts a
JOIN Applications app ON a.AppId = app.AppId
JOIN Users u ON a.UserId = u.UserId
ORDER BY a.CreatedDate DESC`);
res.json({ success: true, data: result.recordset });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Create account
app.post('/api/accounts', async (req, res) => {
try {
const { userId, appId, accountUsername, accountPassword, email, accessLevel, notes } = req.body;
const result = await pool.request()
.input('userId', sql.Int, userId)
.input('appId', sql.Int, appId)
.input('accountUsername', sql.NVarChar, accountUsername)
.input('accountPassword', sql.NVarChar, accountPassword)
.input('email', sql.NVarChar, email)
.input('accessLevel', sql.NVarChar, accessLevel)
.input('notes', sql.NVarChar, notes)
.query(`INSERT INTO Accounts (UserId, AppId, AccountUsername, AccountPassword, Email, AccessLevel, Notes)
VALUES (@userId, @appId, @accountUsername, @accountPassword, @email, @accessLevel, @notes);
SELECT SCOPE_IDENTITY() as AccountId`);
res.json({ success: true, message: 'Account created', accountId: result.recordset[0].AccountId });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Update account
app.put('/api/accounts/:id', async (req, res) => {
try {
const { userId, appId, accountUsername, accountPassword, email, accessLevel, notes } = req.body;
await pool.request()
.input('accountId', sql.Int, req.params.id)
.input('userId', sql.Int, userId)
.input('appId', sql.Int, appId)
.input('accountUsername', sql.NVarChar, accountUsername)
.input('accountPassword', sql.NVarChar, accountPassword)
.input('email', sql.NVarChar, email)
.input('accessLevel', sql.NVarChar, accessLevel)
.input('notes', sql.NVarChar, notes)
.query(`UPDATE Accounts
SET UserId = @userId,
AppId = @appId,
AccountUsername = @accountUsername,
AccountPassword = @accountPassword,
Email = @email,
AccessLevel = @accessLevel,
Notes = @notes,
UpdatedDate = GETDATE()
WHERE AccountId = @accountId`);
res.json({ success: true, message: 'Account updated' });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Delete account
app.delete('/api/accounts/:id', async (req, res) => {
try {
await pool.request()
.input('accountId', sql.Int, req.params.id)
.query('DELETE FROM Accounts WHERE AccountId = @accountId');
res.json({ success: true, message: 'Account deleted' });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// ==========================================
// API ROUTES - Database Info
// ==========================================
// Get database information
app.get('/api/database/info', async (req, res) => {
try {
const tables = await pool.request().query(`
SELECT TABLE_NAME as TableName,
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = t.TABLE_NAME) as ColumnCount
FROM INFORMATION_SCHEMA.TABLES t
WHERE TABLE_SCHEMA = 'dbo'
ORDER BY TABLE_NAME
`);
const users = await pool.request().query('SELECT COUNT(*) as Count FROM Users');
const apps = await pool.request().query('SELECT COUNT(*) as Count FROM Applications');
const accounts = await pool.request().query('SELECT COUNT(*) as Count FROM Accounts');
const assets = await pool.request().query('SELECT COUNT(*) as Count FROM AssetInventory');
res.json({
success: true,
database: 'AccManager',
server: '172.20.235.176',
tables: tables.recordset,
statistics: {
users: users.recordset[0].Count,
applications: apps.recordset[0].Count,
accounts: accounts.recordset[0].Count,
assets: assets.recordset[0].Count
}
});
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// Health check
app.get('/api/health', (req, res) => {
res.json({ status: 'OK', database: 'Connected' });
});
// ==========================================
// Error Handling
// ==========================================
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ success: false, message: err.message });
});
// ==========================================
// Server Startup
// ==========================================
async function startServer() {
try {
await initializeDatabase();
app.listen(PORT, () => {
console.log(`\n========================================`);
console.log(`AccManager Backend Server`);
console.log(`========================================`);
console.log(`✓ Server running on http://localhost:${PORT}`);
console.log(`✓ Database: AccManager`);
console.log(`✓ Default admin: admin / admin`);
console.log(`\nAPI Endpoints:`);
console.log(` POST /api/auth/login`);
console.log(` GET /api/database/info`);
console.log(` GET /api/users`);
console.log(` GET /api/applications`);
console.log(` GET /api/accounts/user/:userId`);
console.log(` GET /api/assets`);
console.log(`========================================\n`);
});
} catch (err) {
console.error('Failed to start server:', err);
process.exit(1);
}
}
// Graceful shutdown
process.on('SIGINT', async () => {
console.log('\nShutting down...');
if (pool) {
await pool.close();
}
process.exit(0);
});
startServer();