// 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 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'; 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; } } // Middleware app.use(cors()); app.use(express.json()); // 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 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);`); // 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) VALUES (@username, @password, @viewPassword, @email, @fullname, @role, 1)`); 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 FROM Users WHERE Username = @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' }); } // 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) { return res.status(400).json({ success: false, message: 'Username and password are required' }); } // Prevent duplicate usernames const existing = await pool.request() .input('username', sql.NVarChar, username) .query('SELECT UserId FROM Users WHERE Username = @username'); if (existing.recordset.length > 0) { return res.status(409).json({ success: false, message: 'Username already exists' }); } const hashedPassword = await hashPassword(password); const viewPassword = encryptPasswordForView(password); const safeFullname = fullname && fullname.trim() ? fullname.trim() : username; 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, username) .input('password', sql.NVarChar, hashedPassword) .input('email', sql.NVarChar, email || null) .input('fullname', sql.NVarChar, safeFullname) .input('roleId', sql.Int, guestRoleId) .input('role', sql.NVarChar, guestRoleName) .input('viewPassword', sql.NVarChar, viewPassword) .query(`INSERT INTO Users (Username, Password, ViewPassword, Email, FullName, RoleId, Role, Status, IsActive) OUTPUT INSERTED.UserId, INSERTED.Username, INSERTED.Email, INSERTED.FullName, INSERTED.Role, INSERTED.RoleId VALUES (@username, @password, @viewPassword, @email, @fullname, @roleId, @role, 'Active', 1)`); } else { result = await pool.request() .input('username', sql.NVarChar, username) .input('password', sql.NVarChar, hashedPassword) .input('email', sql.NVarChar, email || null) .input('fullname', sql.NVarChar, safeFullname) .input('role', sql.NVarChar, guestRoleName) .input('viewPassword', sql.NVarChar, viewPassword) .query(`INSERT INTO Users (Username, Password, ViewPassword, Email, FullName, Role, Status, IsActive) OUTPUT INSERTED.UserId, INSERTED.Username, INSERTED.Email, INSERTED.FullName, INSERTED.Role VALUES (@username, @password, @viewPassword, @email, @fullname, @role, 'Active', 1)`); } const inserted = result.recordset[0]; const user = { ...inserted, role: inserted.Role || guestRoleName || 'guest' }; // 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)`); } res.json({ success: true, message: 'Registration successful', user }); } catch (err) { console.error('Registration error:', err); res.status(500).json({ success: false, message: 'Registration failed' }); } }); // Middleware for role-based access control const requireAdmin = (req, res, next) => { const userRole = req.headers['x-user-role'] || req.query.userRole; if (userRole !== 'admin') { return res.status(403).json({ success: false, message: 'Admin access required' }); } next(); }; // ========================================== // 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 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 }); } }); // 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'); 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 } }); } 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 // ========================================== const PORT = process.env.PORT || 3000; 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(`========================================\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();