done ver1.0.0

This commit is contained in:
2026-04-02 11:16:18 +07:00
parent 58dbefa155
commit d09ba3d2ad
21 changed files with 3271 additions and 668 deletions

View File

@@ -4,8 +4,87 @@
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());
@@ -22,20 +101,20 @@ app.get('/', (req, res) => {
// SQL Server Configuration
const sqlConfig = {
server: '172.20.235.176',
server: DB_SERVER,
authentication: {
type: 'default',
options: {
userName: 'sa',
password: 'robotics@2022'
userName: DB_USER,
password: DB_PASSWORD
}
},
options: {
database: 'AccManager',
trustServerCertificate: true,
database: DB_NAME,
trustServerCertificate: DB_TRUST_CERTIFICATE,
enableKeepAlive: true,
connectTimeout: 30000,
encrypt: false
connectTimeout: DB_CONNECT_TIMEOUT,
encrypt: DB_ENCRYPT
}
};
@@ -50,9 +129,14 @@ async function initializeDatabase() {
// Check and create database if not exists
const masterConnection = new sql.ConnectionPool({
server: '172.20.235.176',
authentication: { type: 'default', options: { userName: 'sa', password: 'robotics@2022' } },
options: { connectTimeout: 30000, database: 'master', trustServerCertificate: true, encrypt: false }
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();
@@ -65,6 +149,7 @@ async function initializeDatabase() {
// Now create tables in AccManager
await createTables();
await migrateLegacyPasswords();
console.log('✓ Database and tables created');
} catch (err) {
@@ -73,6 +158,47 @@ async function initializeDatabase() {
}
}
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
@@ -157,6 +283,7 @@ async function createTables() {
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) {
@@ -165,15 +292,18 @@ async function createTables() {
// 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, '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, Email, FullName, Role, IsActive)
VALUES (@username, @password, @email, @fullname, @role, 1)`);
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);
@@ -205,14 +335,41 @@ async function createTables() {
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)
.input('password', sql.NVarChar, password)
.query('SELECT UserId, Username, Email, FullName, Role, Status FROM Users WHERE Username = @username AND Password = @password AND IsActive = 1');
.query('SELECT UserId, Username, Email, FullName, Role, RoleId, Status, Password FROM Users WHERE Username = @username AND IsActive = 1');
if (result.recordset.length > 0) {
const user = result.recordset[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()
@@ -236,6 +393,217 @@ app.post('/api/auth/login', async (req, res) => {
}
});
// 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
// ==========================================
@@ -244,7 +612,11 @@ app.post('/api/auth/login', async (req, res) => {
app.get('/api/users', async (req, res) => {
try {
const result = await pool.request()
.query('SELECT UserId, Username, Email, FullName, Role, Status, CreatedDate FROM Users ORDER BY CreatedDate DESC');
.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 });
@@ -252,14 +624,27 @@ app.get('/api/users', async (req, res) => {
});
// Get user by ID
app.get('/api/users/:id', async (req, res) => {
app.get('/api/users/:id', requireAdmin, async (req, res) => {
try {
const result = await pool.request()
.input('userId', sql.Int, req.params.id)
.query('SELECT * FROM Users WHERE UserId = @userId');
.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) {
res.json({ success: true, data: result.recordset[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' });
}
@@ -268,22 +653,125 @@ app.get('/api/users/:id', async (req, res) => {
}
});
// Create new user
app.post('/api/users', async (req, res) => {
// Create new user (Admin only)
app.post('/api/users', requireAdmin, async (req, res) => {
try {
const { username, password, email, fullname, role } = req.body;
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, password)
.input('email', sql.NVarChar, email)
.input('password', sql.NVarChar, hashedPassword)
.input('email', sql.NVarChar, email || null)
.input('fullname', sql.NVarChar, fullname)
.input('role', sql.NVarChar, role)
.query(`INSERT INTO Users (Username, Password, Email, FullName, Role, IsActive)
VALUES (@username, @password, @email, @fullname, @role, 1);
SELECT SCOPE_IDENTITY() as UserId`);
.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`);
res.json({ success: true, message: 'User created', userId: result.recordset[0].UserId });
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 });
}
@@ -377,9 +865,10 @@ 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
.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 });
@@ -388,6 +877,21 @@ app.get('/api/accounts/user/:userId', async (req, res) => {
}
});
// 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 {