done ver1.0.0
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user