This commit is contained in:
2026-05-15 14:26:30 +07:00
parent 41e523ff35
commit 1ff9826056
6 changed files with 315 additions and 123 deletions

View File

@@ -3,6 +3,8 @@ FROM node:20-bookworm-slim
WORKDIR /app
ENV NODE_ENV=production
ENV TZ=Asia/Ho_Chi_Minh
ENV APP_TIME_ZONE=Asia/Ho_Chi_Minh
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
@@ -12,4 +14,4 @@ COPY public ./public
EXPOSE 3000
CMD ["node", "backend/server.js"]
CMD ["node", "backend/server.js"]

View File

@@ -10,10 +10,14 @@ const nodemailer = require('nodemailer');
const multer = require('multer');
const XLSX = require('xlsx');
const dotenv = require('dotenv');
const app = express();
dotenv.config();
const APP_TIME_ZONE = process.env.APP_TIME_ZONE || process.env.TZ || 'Asia/Ho_Chi_Minh';
process.env.TZ = APP_TIME_ZONE;
const app = express();
function envBool(name, defaultValue) {
const value = process.env[name];
if (value === undefined) {
@@ -48,6 +52,52 @@ const PASSWORD_RESET_TOKEN_TTL_MINUTES = Number(process.env.PASSWORD_RESET_TOKEN
let mailTransporter;
const appTimePartsFormatter = new Intl.DateTimeFormat('en-CA', {
timeZone: APP_TIME_ZONE,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hourCycle: 'h23'
});
function getAppTimeParts(value = new Date()) {
const date = value instanceof Date ? value : new Date(value);
if (Number.isNaN(date.getTime())) {
return null;
}
return appTimePartsFormatter.formatToParts(date).reduce((parts, part) => {
if (part.type !== 'literal') {
parts[part.type] = part.value;
}
return parts;
}, {});
}
function formatAppTimestampForCode(value = new Date(), includeMilliseconds = false) {
const date = value instanceof Date ? value : new Date(value);
const parts = getAppTimeParts(date);
if (!parts) {
return '';
}
const timestamp = [
parts.year,
parts.month,
parts.day,
parts.hour,
parts.minute,
parts.second
].join('');
return includeMilliseconds
? `${timestamp}${String(date.getMilliseconds()).padStart(3, '0')}`
: timestamp;
}
function isBcryptHash(value) {
return typeof value === 'string' && /^\$2[aby]\$\d{2}\$[./A-Za-z0-9]{53}$/.test(value);
}
@@ -1113,16 +1163,7 @@ function generateManualAssetCodeFromPayload(payload = {}) {
const fromSerial = sanitizeAssetCodeToken(payload.serialNumber);
const fromName = sanitizeAssetCodeToken(payload.assetName);
const base = (fromModel || fromSerial || fromName || 'ASSET').slice(0, 32);
const now = new Date();
const timestamp = [
String(now.getFullYear()),
String(now.getMonth() + 1).padStart(2, '0'),
String(now.getDate()).padStart(2, '0'),
String(now.getHours()).padStart(2, '0'),
String(now.getMinutes()).padStart(2, '0'),
String(now.getSeconds()).padStart(2, '0'),
String(now.getMilliseconds()).padStart(3, '0')
].join('');
const timestamp = formatAppTimestampForCode(new Date(), true);
const randomSuffix = String(Math.floor(Math.random() * 100)).padStart(2, '0');
return `AST-${base}-${timestamp}${randomSuffix}`;
}
@@ -1598,7 +1639,8 @@ const sqlConfig = {
trustServerCertificate: DB_TRUST_CERTIFICATE,
enableKeepAlive: true,
connectTimeout: DB_CONNECT_TIMEOUT,
encrypt: DB_ENCRYPT
encrypt: DB_ENCRYPT,
useUTC: false
}
};
@@ -1619,7 +1661,8 @@ async function initializeDatabase() {
connectTimeout: DB_CONNECT_TIMEOUT,
database: 'master',
trustServerCertificate: DB_TRUST_CERTIFICATE,
encrypt: DB_ENCRYPT
encrypt: DB_ENCRYPT,
useUTC: false
}
});
@@ -1683,6 +1726,97 @@ async function migrateLegacyPasswords() {
}
}
async function ensureAppTimeDefaultConstraints() {
await pool.request().query(`
DECLARE @schemaName SYSNAME = N'dbo';
DECLARE @defaults TABLE (
TableName SYSNAME NOT NULL,
ColumnName SYSNAME NOT NULL,
ConstraintName SYSNAME NOT NULL,
Definition NVARCHAR(MAX) NOT NULL
);
INSERT INTO @defaults (TableName, ColumnName, ConstraintName, Definition)
VALUES
(N'Users', N'CreatedDate', N'DF_Users_CreatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'Applications', N'CreatedDate', N'DF_Applications_CreatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'Applications', N'UpdatedDate', N'DF_Applications_UpdatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'Accounts', N'CreatedDate', N'DF_Accounts_CreatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'Accounts', N'UpdatedDate', N'DF_Accounts_UpdatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetInventory', N'CreatedDate', N'DF_AssetInventory_CreatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetInventory', N'UpdatedDate', N'DF_AssetInventory_UpdatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetDepartments', N'CreatedDate', N'DF_AssetDepartments_CreatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetDepartments', N'UpdatedDate', N'DF_AssetDepartments_UpdatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetProjects', N'CreatedDate', N'DF_AssetProjects_CreatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetProjects', N'UpdatedDate', N'DF_AssetProjects_UpdatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetBorrowRequests', N'BorrowDate', N'DF_AssetBorrowRequests_BorrowDate', N'(CAST(DATEADD(HOUR, 7, SYSUTCDATETIME()) AS DATE))'),
(N'AssetBorrowRequests', N'CreatedDate', N'DF_AssetBorrowRequests_CreatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetBorrowRequests', N'UpdatedDate', N'DF_AssetBorrowRequests_UpdatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetBorrowRequestLinks', N'CreatedDate', N'DF_AssetBorrowRequestLinks_CreatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetExportHistory', N'ExportedDate', N'DF_AssetExportHistory_ExportedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetExportHistory', N'CreatedDate', N'DF_AssetExportHistory_CreatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetExportHistory', N'UpdatedDate', N'DF_AssetExportHistory_UpdatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetDamageDisposalHistory', N'ActionDate', N'DF_AssetDamageDisposalHistory_ActionDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetDamageDisposalHistory', N'CreatedDate', N'DF_AssetDamageDisposalHistory_CreatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetDamageDisposalHistory', N'UpdatedDate', N'DF_AssetDamageDisposalHistory_UpdatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AuditLog', N'Timestamp', N'DF_AuditLog_Timestamp', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))');
DECLARE @tableName SYSNAME;
DECLARE @columnName SYSNAME;
DECLARE @constraintName SYSNAME;
DECLARE @definition NVARCHAR(MAX);
DECLARE @existingName SYSNAME;
DECLARE @objectId INT;
DECLARE @sql NVARCHAR(MAX);
DECLARE default_cursor CURSOR LOCAL FAST_FORWARD FOR
SELECT TableName, ColumnName, ConstraintName, Definition
FROM @defaults;
OPEN default_cursor;
FETCH NEXT FROM default_cursor INTO @tableName, @columnName, @constraintName, @definition;
WHILE @@FETCH_STATUS = 0
BEGIN
SET @objectId = OBJECT_ID(QUOTENAME(@schemaName) + N'.' + QUOTENAME(@tableName), N'U');
IF @objectId IS NOT NULL
AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = @objectId AND name = @columnName)
BEGIN
SET @existingName = NULL;
SELECT @existingName = dc.name
FROM sys.default_constraints dc
INNER JOIN sys.columns c
ON c.object_id = dc.parent_object_id
AND c.column_id = dc.parent_column_id
WHERE dc.parent_object_id = @objectId
AND c.name = @columnName;
IF @existingName IS NOT NULL
BEGIN
SET @sql = N'ALTER TABLE '
+ QUOTENAME(@schemaName) + N'.' + QUOTENAME(@tableName)
+ N' DROP CONSTRAINT ' + QUOTENAME(@existingName);
EXEC sp_executesql @sql;
END
SET @sql = N'ALTER TABLE '
+ QUOTENAME(@schemaName) + N'.' + QUOTENAME(@tableName)
+ N' ADD CONSTRAINT ' + QUOTENAME(@constraintName)
+ N' DEFAULT ' + @definition
+ N' FOR ' + QUOTENAME(@columnName);
EXEC sp_executesql @sql;
END
FETCH NEXT FROM default_cursor INTO @tableName, @columnName, @constraintName, @definition;
END
CLOSE default_cursor;
DEALLOCATE default_cursor;
`);
}
async function createTables() {
const queries = [
// Users Table
@@ -1696,7 +1830,7 @@ async function createTables() {
FullName NVARCHAR(100),
Role NVARCHAR(50) NOT NULL,
Status NVARCHAR(20) DEFAULT 'Active',
CreatedDate DATETIME DEFAULT GETDATE(),
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
LastLogin DATETIME,
IsActive BIT DEFAULT 1
)
@@ -1713,8 +1847,8 @@ async function createTables() {
Icon NVARCHAR(50),
Description NVARCHAR(500),
Url NVARCHAR(255),
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE()
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME()))
)
END`,
@@ -1731,8 +1865,8 @@ async function createTables() {
AccessLevel NVARCHAR(50),
Status NVARCHAR(20) DEFAULT 'Active',
Notes NVARCHAR(MAX),
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE(),
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
FOREIGN KEY (UserId) REFERENCES Users(UserId) ON DELETE CASCADE,
FOREIGN KEY (AppId) REFERENCES Applications(AppId) ON DELETE CASCADE
)
@@ -1765,8 +1899,8 @@ async function createTables() {
Status NVARCHAR(30) NOT NULL DEFAULT 'in_use',
Notes NVARCHAR(MAX),
CreatedBy INT NULL,
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE(),
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL
)
END`,
@@ -1777,8 +1911,8 @@ async function createTables() {
CREATE TABLE AssetDepartments (
DepartmentId INT PRIMARY KEY IDENTITY(1,1),
DepartmentName NVARCHAR(100) NOT NULL,
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE()
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME()))
)
END`,
@@ -1788,8 +1922,8 @@ async function createTables() {
CREATE TABLE AssetProjects (
ProjectId INT PRIMARY KEY IDENTITY(1,1),
ProjectName NVARCHAR(150) NOT NULL,
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE()
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME()))
)
END`,
@@ -1805,15 +1939,15 @@ async function createTables() {
BorrowQuantity INT NOT NULL DEFAULT 1,
ReturnedQuantity INT NOT NULL DEFAULT 0,
Unit NVARCHAR(50),
BorrowDate DATE NOT NULL DEFAULT CAST(GETDATE() AS DATE),
BorrowDate DATE NOT NULL DEFAULT (CAST(DATEADD(HOUR, 7, SYSUTCDATETIME()) AS DATE)),
RequestNote NVARCHAR(500) NULL,
RejectReason NVARCHAR(1000) NULL,
CreatedBy INT NULL,
ProcessedBy INT NULL,
ProcessedByName NVARCHAR(100) NULL,
ProcessedDate DATETIME NULL,
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE(),
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE,
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL,
FOREIGN KEY (ProcessedBy) REFERENCES Users(UserId) ON DELETE SET NULL
@@ -1828,7 +1962,7 @@ async function createTables() {
BorrowId INT NOT NULL,
ReturnId INT NOT NULL,
Quantity INT NOT NULL DEFAULT 1,
CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
CreatedDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
FOREIGN KEY (BorrowId) REFERENCES AssetBorrowRequests(BorrowId) ON DELETE NO ACTION,
FOREIGN KEY (ReturnId) REFERENCES AssetBorrowRequests(BorrowId) ON DELETE NO ACTION
)
@@ -1848,9 +1982,9 @@ async function createTables() {
ExportedByName NVARCHAR(100) NOT NULL,
ExportNote NVARCHAR(1000) NULL,
CreatedBy INT NULL,
ExportedDate DATETIME NOT NULL DEFAULT GETDATE(),
CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
UpdatedDate DATETIME NOT NULL DEFAULT GETDATE(),
ExportedDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
CreatedDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
UpdatedDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE
)
END`,
@@ -1882,9 +2016,9 @@ async function createTables() {
ActionNote NVARCHAR(1000) NULL,
CreatedBy INT NULL,
CreatedByName NVARCHAR(100) NULL,
ActionDate DATETIME NOT NULL DEFAULT GETDATE(),
CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
UpdatedDate DATETIME NOT NULL DEFAULT GETDATE(),
ActionDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
CreatedDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
UpdatedDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE,
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL
)
@@ -1901,7 +2035,7 @@ async function createTables() {
RecordId INT,
OldValue NVARCHAR(MAX),
NewValue NVARCHAR(MAX),
Timestamp DATETIME DEFAULT GETDATE(),
Timestamp DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
FOREIGN KEY (UserId) REFERENCES Users(UserId)
)
END`
@@ -1988,7 +2122,7 @@ async function createTables() {
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','Borrower') IS NULL ALTER TABLE AssetInventory ADD Borrower NVARCHAR(255) NULL;`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','ExportedBy') IS NULL ALTER TABLE AssetInventory ADD ExportedBy NVARCHAR(100) NULL;`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','Unit') IS NULL ALTER TABLE AssetBorrowRequests ADD Unit NVARCHAR(50) NULL;`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','BorrowDate') IS NULL ALTER TABLE AssetBorrowRequests ADD BorrowDate DATE NOT NULL CONSTRAINT DF_AssetBorrowRequests_BorrowDate DEFAULT(CAST(GETDATE() AS DATE));`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','BorrowDate') IS NULL ALTER TABLE AssetBorrowRequests ADD BorrowDate DATE NOT NULL CONSTRAINT DF_AssetBorrowRequests_BorrowDate DEFAULT(CAST(DATEADD(HOUR, 7, SYSUTCDATETIME()) AS DATE));`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','RequestType') IS NULL ALTER TABLE AssetBorrowRequests ADD RequestType NVARCHAR(20) NOT NULL CONSTRAINT DF_AssetBorrowRequests_RequestType DEFAULT('borrow');`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','RequestStatus') IS NULL ALTER TABLE AssetBorrowRequests ADD RequestStatus NVARCHAR(20) NOT NULL CONSTRAINT DF_AssetBorrowRequests_RequestStatus DEFAULT('approved');`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','ReturnedQuantity') IS NULL ALTER TABLE AssetBorrowRequests ADD ReturnedQuantity INT NOT NULL CONSTRAINT DF_AssetBorrowRequests_ReturnedQuantity DEFAULT(0);`);
@@ -2007,7 +2141,7 @@ async function createTables() {
ADD CONSTRAINT FK_AssetBorrowRequests_ProcessedBy
FOREIGN KEY (ProcessedBy) REFERENCES Users(UserId) ON DELETE SET NULL;
`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','UpdatedDate') IS NULL ALTER TABLE AssetBorrowRequests ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetBorrowRequests_UpdatedDate DEFAULT(GETDATE());`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','UpdatedDate') IS NULL ALTER TABLE AssetBorrowRequests ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetBorrowRequests_UpdatedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME()));`);
await pool.request().query(`
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetBorrowRequestLinks')
BEGIN
@@ -2016,7 +2150,7 @@ async function createTables() {
BorrowId INT NOT NULL,
ReturnId INT NOT NULL,
Quantity INT NOT NULL CONSTRAINT DF_AssetBorrowRequestLinks_Quantity DEFAULT(1),
CreatedDate DATETIME NOT NULL CONSTRAINT DF_AssetBorrowRequestLinks_CreatedDate DEFAULT(GETDATE()),
CreatedDate DATETIME NOT NULL CONSTRAINT DF_AssetBorrowRequestLinks_CreatedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME())),
FOREIGN KEY (BorrowId) REFERENCES AssetBorrowRequests(BorrowId) ON DELETE NO ACTION,
FOREIGN KEY (ReturnId) REFERENCES AssetBorrowRequests(BorrowId) ON DELETE NO ACTION
);
@@ -2030,9 +2164,9 @@ async function createTables() {
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','ExportedByName') IS NULL ALTER TABLE AssetExportHistory ADD ExportedByName NVARCHAR(100) NULL;`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','ExportNote') IS NULL ALTER TABLE AssetExportHistory ADD ExportNote NVARCHAR(1000) NULL;`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','CreatedBy') IS NULL ALTER TABLE AssetExportHistory ADD CreatedBy INT NULL;`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','ExportedDate') IS NULL ALTER TABLE AssetExportHistory ADD ExportedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_ExportedDate DEFAULT(GETDATE());`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','CreatedDate') IS NULL ALTER TABLE AssetExportHistory ADD CreatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_CreatedDate DEFAULT(GETDATE());`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','UpdatedDate') IS NULL ALTER TABLE AssetExportHistory ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_UpdatedDate DEFAULT(GETDATE());`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','ExportedDate') IS NULL ALTER TABLE AssetExportHistory ADD ExportedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_ExportedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME()));`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','CreatedDate') IS NULL ALTER TABLE AssetExportHistory ADD CreatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_CreatedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME()));`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','UpdatedDate') IS NULL ALTER TABLE AssetExportHistory ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_UpdatedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME()));`);
await pool.request().query(`
IF NOT EXISTS (
SELECT 1
@@ -2102,7 +2236,7 @@ async function createTables() {
THEN 'returned'
ELSE borrowRows.RequestStatus
END,
UpdatedDate = GETDATE()
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
FROM AssetBorrowRequests borrowRows
INNER JOIN (
SELECT links.BorrowId, SUM(ISNULL(links.Quantity, 0)) AS ReturnedQuantity
@@ -2164,13 +2298,19 @@ async function createTables() {
await pool.request().query(`IF COL_LENGTH('dbo.Users','EmailVerifyTokenExpires') IS NULL ALTER TABLE Users ADD EmailVerifyTokenExpires DATETIME NULL;`);
await pool.request().query(`IF COL_LENGTH('dbo.Users','PasswordResetToken') IS NULL ALTER TABLE Users ADD PasswordResetToken NVARCHAR(255) NULL;`);
await pool.request().query(`IF COL_LENGTH('dbo.Users','PasswordResetTokenExpires') IS NULL ALTER TABLE Users ADD PasswordResetTokenExpires DATETIME NULL;`);
await pool.request().query(`UPDATE Users SET EmailVerified = 1, EmailVerifiedAt = ISNULL(EmailVerifiedAt, GETDATE()) WHERE LOWER(ISNULL(Role, '')) = 'admin';`);
await pool.request().query(`UPDATE Users SET EmailVerified = 1, EmailVerifiedAt = ISNULL(EmailVerifiedAt, DATEADD(HOUR, 7, SYSUTCDATETIME())) 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);
}
try {
await ensureAppTimeDefaultConstraints();
} catch (err) {
console.error('Timezone default constraint error:', err.message);
}
// Sync legacy departments from AssetInventory to AssetDepartments
try {
await syncAssetDepartmentsFromInventory();
@@ -2198,11 +2338,11 @@ async function createTables() {
.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())
VALUES (@username, @password, @viewPassword, @email, @fullname, @role, 1, 1, DATEADD(HOUR, 7, SYSUTCDATETIME()))
ELSE
UPDATE Users
SET EmailVerified = 1,
EmailVerifiedAt = ISNULL(EmailVerifiedAt, GETDATE())
EmailVerifiedAt = ISNULL(EmailVerifiedAt, DATEADD(HOUR, 7, SYSUTCDATETIME()))
WHERE Username = @username`);
console.log('[OK] Admin user created: admin / admin');
} catch (err) {
@@ -2287,7 +2427,7 @@ app.post('/api/auth/login', async (req, res) => {
// Update last login
await pool.request()
.input('userId', sql.Int, user.UserId)
.query('UPDATE Users SET LastLogin = GETDATE() WHERE UserId = @userId');
.query('UPDATE Users SET LastLogin = DATEADD(HOUR, 7, SYSUTCDATETIME()) WHERE UserId = @userId');
res.json({
success: true,
@@ -2387,7 +2527,7 @@ app.post('/api/auth/register', async (req, res) => {
.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()))`);
VALUES (@username, @password, @viewPassword, @email, @fullname, @roleId, @role, 'Active', 1, 0, @emailVerifyToken, DATEADD(MINUTE, @tokenTtlMinutes, DATEADD(HOUR, 7, SYSUTCDATETIME())))`);
} else {
result = await pool.request()
.input('username', sql.NVarChar, safeUsername)
@@ -2400,7 +2540,7 @@ app.post('/api/auth/register', async (req, res) => {
.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()))`);
VALUES (@username, @password, @viewPassword, @email, @fullname, @role, 'Active', 1, 0, @emailVerifyToken, DATEADD(MINUTE, @tokenTtlMinutes, DATEADD(HOUR, 7, SYSUTCDATETIME())))`);
}
const inserted = result.recordset[0];
@@ -2475,7 +2615,7 @@ app.get('/api/auth/verify-email', async (req, res) => {
.input('userId', sql.Int, verifiedUser.UserId)
.query(`UPDATE Users
SET EmailVerified = 1,
EmailVerifiedAt = GETDATE(),
EmailVerifiedAt = DATEADD(HOUR, 7, SYSUTCDATETIME()),
EmailVerifyToken = NULL,
EmailVerifyTokenExpires = NULL
WHERE UserId = @userId`);
@@ -2493,7 +2633,7 @@ app.get('/api/auth/verify-email', async (req, res) => {
await pool.request()
.input('userId', sql.Int, verifiedUser.UserId)
.query('UPDATE Users SET LastLogin = GETDATE() WHERE UserId = @userId');
.query('UPDATE Users SET LastLogin = DATEADD(HOUR, 7, SYSUTCDATETIME()) WHERE UserId = @userId');
res.json({
success: true,
@@ -2540,7 +2680,7 @@ app.post('/api/auth/resend-verification', async (req, res) => {
.input('tokenTtlMinutes', sql.Int, EMAIL_VERIFY_TOKEN_TTL_MINUTES)
.query(`UPDATE Users
SET EmailVerifyToken = @tokenHash,
EmailVerifyTokenExpires = DATEADD(MINUTE, @tokenTtlMinutes, GETDATE())
EmailVerifyTokenExpires = DATEADD(MINUTE, @tokenTtlMinutes, DATEADD(HOUR, 7, SYSUTCDATETIME()))
WHERE UserId = @userId`);
const emailResult = await sendVerificationEmail({
@@ -2607,7 +2747,7 @@ app.post('/api/auth/forgot-password', async (req, res) => {
.input('tokenTtlMinutes', sql.Int, PASSWORD_RESET_TOKEN_TTL_MINUTES)
.query(`UPDATE Users
SET PasswordResetToken = @tokenHash,
PasswordResetTokenExpires = DATEADD(MINUTE, @tokenTtlMinutes, GETDATE())
PasswordResetTokenExpires = DATEADD(MINUTE, @tokenTtlMinutes, DATEADD(HOUR, 7, SYSUTCDATETIME()))
WHERE UserId = @userId`);
const emailResult = await sendPasswordResetEmail({
@@ -2946,7 +3086,7 @@ app.put('/api/users/me', async (req, res) => {
SET FullName = @fullname,
Email = @email
${shouldChangePassword ? ', Password = @password, ViewPassword = @viewPassword' : ''}
${emailChanged ? ', EmailVerified = 0, EmailVerifiedAt = NULL, EmailVerifyToken = @emailVerifyToken, EmailVerifyTokenExpires = DATEADD(MINUTE, @tokenTtlMinutes, GETDATE())' : ''}
${emailChanged ? ', EmailVerified = 0, EmailVerifiedAt = NULL, EmailVerifyToken = @emailVerifyToken, EmailVerifyTokenExpires = DATEADD(MINUTE, @tokenTtlMinutes, DATEADD(HOUR, 7, SYSUTCDATETIME()))' : ''}
WHERE UserId = @userId`);
let emailResult = { sent: true };
@@ -3201,7 +3341,7 @@ app.put('/api/applications/:id', async (req, res) => {
Icon = @icon,
Description = @description,
Url = @url,
UpdatedDate = GETDATE()
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE AppId = @appId`);
res.json({ success: true, message: 'Application updated' });
@@ -3304,7 +3444,7 @@ app.put('/api/accounts/:id', async (req, res) => {
Email = @email,
AccessLevel = @accessLevel,
Notes = @notes,
UpdatedDate = GETDATE()
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE AccountId = @accountId`);
res.json({ success: true, message: 'Account updated' });
@@ -3454,7 +3594,7 @@ app.put('/api/asset-departments/:id', requireAssetOrAdmin, async (req, res) => {
.query(`
UPDATE AssetDepartments
SET DepartmentName = @departmentName,
UpdatedDate = GETDATE()
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE DepartmentId = @departmentId
`);
@@ -3464,7 +3604,7 @@ app.put('/api/asset-departments/:id', requireAssetOrAdmin, async (req, res) => {
.query(`
UPDATE AssetInventory
SET Department = @newDepartmentName,
UpdatedDate = GETDATE()
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE LOWER(LTRIM(RTRIM(Department))) = LOWER(@oldDepartmentName)
`);
@@ -3518,7 +3658,7 @@ app.delete('/api/asset-departments/:id', requireAssetOrAdmin, async (req, res) =
.query(`
UPDATE AssetInventory
SET Department = NULL,
UpdatedDate = GETDATE()
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE LOWER(LTRIM(RTRIM(Department))) = LOWER(@departmentName)
`);
@@ -3672,7 +3812,7 @@ app.put('/api/asset-projects/:id', requireAssetOrAdmin, async (req, res) => {
.query(`
UPDATE AssetProjects
SET ProjectName = @projectName,
UpdatedDate = GETDATE()
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE ProjectId = @projectId
`);
@@ -3682,7 +3822,7 @@ app.put('/api/asset-projects/:id', requireAssetOrAdmin, async (req, res) => {
.query(`
UPDATE AssetInventory
SET Project = @newProjectName,
UpdatedDate = GETDATE()
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE LOWER(LTRIM(RTRIM(Project))) = LOWER(@oldProjectName)
`);
@@ -3736,7 +3876,7 @@ app.delete('/api/asset-projects/:id', requireAssetOrAdmin, async (req, res) => {
.query(`
UPDATE AssetInventory
SET Project = NULL,
UpdatedDate = GETDATE()
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE LOWER(LTRIM(RTRIM(Project))) = LOWER(@projectName)
`);
@@ -4560,7 +4700,7 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
UsedQuantity = @usedQuantity,
Status = @status,
ExportedBy = @exportedBy,
UpdatedDate = GETDATE()
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE AssetId = @assetId
`);
} else {
@@ -4619,7 +4759,7 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
UsedQuantity = @usedQuantity,
Status = @status,
ExportedBy = CASE WHEN @borrower IS NULL THEN NULL ELSE @exportedBy END,
UpdatedDate = GETDATE()
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE AssetId = @assetId
`);
@@ -4665,7 +4805,7 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
THEN 'returned'
ELSE RequestStatus
END,
UpdatedDate = GETDATE()
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE BorrowId = @borrowId
`);
}
@@ -4742,7 +4882,7 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
THEN 'returned'
ELSE RequestStatus
END,
UpdatedDate = GETDATE()
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE BorrowId = @borrowId
`);
@@ -4764,8 +4904,8 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
RejectReason = @rejectReason,
ProcessedBy = @processedBy,
ProcessedByName = @processedByName,
ProcessedDate = GETDATE(),
UpdatedDate = GETDATE()
ProcessedDate = DATEADD(HOUR, 7, SYSUTCDATETIME()),
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE BorrowId = @borrowId
`);
@@ -5194,7 +5334,7 @@ app.post('/api/assets/:id/damage-disposal', requireAssetOrAdmin, async (req, res
NewQuantity = @newQuantity,
UsedQuantity = @usedQuantity,
Status = @status,
UpdatedDate = GETDATE()
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE AssetId = @assetId
`);
@@ -5449,7 +5589,7 @@ app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => {
Status = @status,
ExportedBy = @exportedBy,
Notes = @notes,
UpdatedDate = GETDATE()
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE AssetId = @assetId
`);
@@ -5651,7 +5791,7 @@ app.put('/api/assets/:id', requireAssetOrAdmin, async (req, res) => {
PurchasePrice = @purchasePrice,
Status = @status,
Notes = @notes,
UpdatedDate = GETDATE()
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE AssetId = @assetId
`);
@@ -5868,7 +6008,7 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
PurchasePrice = @purchasePrice,
Status = @status,
Notes = @notes,
UpdatedDate = GETDATE()
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHEN NOT MATCHED THEN
INSERT (
AssetCode, AssetName, Model, SerialNumber,

View File

@@ -32,7 +32,7 @@ BEGIN
FullName NVARCHAR(100),
Role NVARCHAR(50) NOT NULL,
Status NVARCHAR(20) DEFAULT 'Active',
CreatedDate DATETIME DEFAULT GETDATE(),
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
LastLogin DATETIME,
IsActive BIT DEFAULT 1
);
@@ -51,8 +51,8 @@ BEGIN
Status NVARCHAR(20) DEFAULT 'online',
Icon NVARCHAR(50),
Description NVARCHAR(500),
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE()
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME()))
);
PRINT 'Table Applications created successfully.';
END
@@ -72,8 +72,8 @@ BEGIN
AccessLevel NVARCHAR(50),
Status NVARCHAR(20) DEFAULT 'Active',
Notes NVARCHAR(MAX),
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE(),
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
FOREIGN KEY (UserId) REFERENCES Users(UserId) ON DELETE CASCADE,
FOREIGN KEY (AppId) REFERENCES Applications(AppId) ON DELETE CASCADE
);
@@ -109,8 +109,8 @@ BEGIN
Status NVARCHAR(30) NOT NULL DEFAULT 'in_use',
Notes NVARCHAR(MAX),
CreatedBy INT NULL,
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE(),
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL
);
PRINT 'Table AssetInventory created successfully.';
@@ -175,8 +175,8 @@ BEGIN
CREATE TABLE AssetDepartments (
DepartmentId INT PRIMARY KEY IDENTITY(1,1),
DepartmentName NVARCHAR(100) NOT NULL,
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE()
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME()))
);
PRINT 'Table AssetDepartments created successfully.';
END
@@ -204,8 +204,8 @@ BEGIN
CREATE TABLE AssetProjects (
ProjectId INT PRIMARY KEY IDENTITY(1,1),
ProjectName NVARCHAR(150) NOT NULL,
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE()
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME()))
);
PRINT 'Table AssetProjects created successfully.';
END
@@ -224,15 +224,15 @@ BEGIN
BorrowQuantity INT NOT NULL DEFAULT 1,
ReturnedQuantity INT NOT NULL DEFAULT 0,
Unit NVARCHAR(50),
BorrowDate DATE NOT NULL DEFAULT CAST(GETDATE() AS DATE),
BorrowDate DATE NOT NULL DEFAULT (CAST(DATEADD(HOUR, 7, SYSUTCDATETIME()) AS DATE)),
RequestNote NVARCHAR(500) NULL,
RejectReason NVARCHAR(1000) NULL,
CreatedBy INT NULL,
ProcessedBy INT NULL,
ProcessedByName NVARCHAR(100) NULL,
ProcessedDate DATETIME NULL,
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE(),
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE,
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL,
FOREIGN KEY (ProcessedBy) REFERENCES Users(UserId) ON DELETE SET NULL
@@ -247,12 +247,12 @@ END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'BorrowDate') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD BorrowDate DATE NOT NULL CONSTRAINT DF_AssetBorrowRequests_BorrowDate DEFAULT(CAST(GETDATE() AS DATE));
ALTER TABLE AssetBorrowRequests ADD BorrowDate DATE NOT NULL CONSTRAINT DF_AssetBorrowRequests_BorrowDate DEFAULT(CAST(DATEADD(HOUR, 7, SYSUTCDATETIME()) AS DATE));
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'UpdatedDate') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetBorrowRequests_UpdatedDate DEFAULT(GETDATE());
ALTER TABLE AssetBorrowRequests ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetBorrowRequests_UpdatedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME()));
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'RequestType') IS NULL
@@ -319,7 +319,7 @@ BEGIN
BorrowId INT NOT NULL,
ReturnId INT NOT NULL,
Quantity INT NOT NULL CONSTRAINT DF_AssetBorrowRequestLinks_Quantity DEFAULT(1),
CreatedDate DATETIME NOT NULL CONSTRAINT DF_AssetBorrowRequestLinks_CreatedDate DEFAULT(GETDATE()),
CreatedDate DATETIME NOT NULL CONSTRAINT DF_AssetBorrowRequestLinks_CreatedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME())),
FOREIGN KEY (BorrowId) REFERENCES AssetBorrowRequests(BorrowId) ON DELETE NO ACTION,
FOREIGN KEY (ReturnId) REFERENCES AssetBorrowRequests(BorrowId) ON DELETE NO ACTION
);
@@ -373,7 +373,7 @@ BEGIN
THEN 'returned'
ELSE borrowRows.RequestStatus
END,
UpdatedDate = GETDATE()
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
FROM AssetBorrowRequests borrowRows
INNER JOIN (
SELECT links.BorrowId, SUM(ISNULL(links.Quantity, 0)) AS ReturnedQuantity
@@ -401,9 +401,9 @@ BEGIN
ExportedByName NVARCHAR(100) NOT NULL,
ExportNote NVARCHAR(1000) NULL,
CreatedBy INT NULL,
ExportedDate DATETIME NOT NULL DEFAULT GETDATE(),
CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
UpdatedDate DATETIME NOT NULL DEFAULT GETDATE(),
ExportedDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
CreatedDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
UpdatedDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE
);
PRINT 'Table AssetExportHistory created successfully.';
@@ -451,17 +451,17 @@ END
IF COL_LENGTH('dbo.AssetExportHistory', 'ExportedDate') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD ExportedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_ExportedDate DEFAULT(GETDATE());
ALTER TABLE AssetExportHistory ADD ExportedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_ExportedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME()));
END
IF COL_LENGTH('dbo.AssetExportHistory', 'CreatedDate') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD CreatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_CreatedDate DEFAULT(GETDATE());
ALTER TABLE AssetExportHistory ADD CreatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_CreatedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME()));
END
IF COL_LENGTH('dbo.AssetExportHistory', 'UpdatedDate') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_UpdatedDate DEFAULT(GETDATE());
ALTER TABLE AssetExportHistory ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_UpdatedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME()));
END
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'FK_AssetExportHistory_CreatedBy')
@@ -512,9 +512,9 @@ BEGIN
ActionNote NVARCHAR(1000) NULL,
CreatedBy INT NULL,
CreatedByName NVARCHAR(100) NULL,
ActionDate DATETIME NOT NULL DEFAULT GETDATE(),
CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
UpdatedDate DATETIME NOT NULL DEFAULT GETDATE(),
ActionDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
CreatedDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
UpdatedDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE,
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL
);
@@ -534,7 +534,7 @@ BEGIN
RecordId INT,
OldValue NVARCHAR(MAX),
NewValue NVARCHAR(MAX),
Timestamp DATETIME DEFAULT GETDATE(),
Timestamp DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
FOREIGN KEY (UserId) REFERENCES Users(UserId)
);
PRINT 'Table AuditLog created successfully.';

View File

@@ -8,6 +8,8 @@ services:
environment:
NODE_ENV: ${NODE_ENV:-production}
PORT: 3000
TZ: ${TZ:-Asia/Ho_Chi_Minh}
APP_TIME_ZONE: ${APP_TIME_ZONE:-Asia/Ho_Chi_Minh}
DB_SERVER: ${DB_SERVER:-172.20.235.176}
DB_USER: ${DB_USER:-sa}
DB_PASSWORD: ${DB_PASSWORD:-changeme}
@@ -23,4 +25,4 @@ services:
SMTP_USER: ${SMTP_USER:-}
SMTP_PASS: ${SMTP_PASS:-}
SMTP_FROM: ${SMTP_FROM:-}
EMAIL_VERIFY_TOKEN_TTL_MINUTES: ${EMAIL_VERIFY_TOKEN_TTL_MINUTES:-30}
EMAIL_VERIFY_TOKEN_TTL_MINUTES: ${EMAIL_VERIFY_TOKEN_TTL_MINUTES:-30}

View File

@@ -10,6 +10,8 @@ services:
environment:
NODE_ENV: production
PORT: 3000
TZ: ${TZ:-Asia/Ho_Chi_Minh}
APP_TIME_ZONE: ${APP_TIME_ZONE:-Asia/Ho_Chi_Minh}
DB_SERVER: ${DB_SERVER:-172.20.235.176}
DB_USER: ${DB_USER:-sa}
DB_PASSWORD: ${DB_PASSWORD:-changeme}
@@ -25,4 +27,4 @@ services:
SMTP_USER: ${SMTP_USER:-}
SMTP_PASS: ${SMTP_PASS:-}
SMTP_FROM: ${SMTP_FROM:-}
EMAIL_VERIFY_TOKEN_TTL_MINUTES: ${EMAIL_VERIFY_TOKEN_TTL_MINUTES:-30}
EMAIL_VERIFY_TOKEN_TTL_MINUTES: ${EMAIL_VERIFY_TOKEN_TTL_MINUTES:-30}

View File

@@ -1,6 +1,34 @@
// VaultSentinel - Account Management Application
// Main JavaScript functionality
const APP_TIME_ZONE = 'Asia/Ho_Chi_Minh';
const APP_DATE_FORMATTER = new Intl.DateTimeFormat('vi-VN', {
timeZone: APP_TIME_ZONE,
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
const APP_DATE_TIME_FORMATTER = new Intl.DateTimeFormat('vi-VN', {
timeZone: APP_TIME_ZONE,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hourCycle: 'h23'
});
const APP_TIME_PARTS_FORMATTER = new Intl.DateTimeFormat('en-CA', {
timeZone: APP_TIME_ZONE,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hourCycle: 'h23'
});
class AccountManager {
constructor() {
// Check if user is logged in
@@ -1795,7 +1823,7 @@ class AccountManager {
<div class="bg-surface-container-lowest p-4 rounded-xl border border-outline-variant/15 flex flex-col">
<span class="text-[10px] font-bold text-on-surface-variant uppercase tracking-wider mb-2">Last Updated</span>
<div class="flex items-baseline justify-between">
<span class="text-sm font-black text-on-surface">${new Date().toLocaleDateString()}</span>
<span class="text-sm font-black text-on-surface">${APP_DATE_FORMATTER.format(new Date())}</span>
</div>
</div>
<div class="bg-primary-container/10 p-4 rounded-xl border border-primary/20 flex flex-col">
@@ -2055,21 +2083,49 @@ class AccountManager {
return { label: 'Đang sử dụng', className: 'bg-blue-100 text-blue-700' };
}
getAppTimeParts(value = new Date()) {
const date = value instanceof Date ? value : new Date(value);
if (Number.isNaN(date.getTime())) return null;
return APP_TIME_PARTS_FORMATTER.formatToParts(date).reduce((parts, part) => {
if (part.type !== 'literal') {
parts[part.type] = part.value;
}
return parts;
}, {});
}
formatTimestampForCode(value = new Date(), includeMilliseconds = false) {
const date = value instanceof Date ? value : new Date(value);
const parts = this.getAppTimeParts(date);
if (!parts) return '';
const timestamp = [
parts.year,
parts.month,
parts.day,
parts.hour,
parts.minute,
parts.second
].join('');
return includeMilliseconds
? `${timestamp}${String(date.getMilliseconds()).padStart(3, '0')}`
: timestamp;
}
formatDateOnly(value) {
if (!value) return '-';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return String(value);
return date.toLocaleDateString();
return APP_DATE_FORMATTER.format(date);
}
toDateInputValue(value) {
if (!value) return '';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '';
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
const parts = this.getAppTimeParts(value);
if (!parts) return '';
return `${parts.year}-${parts.month}-${parts.day}`;
}
formatBorrowerDisplay(name, quantity = 1) {
@@ -5019,16 +5075,7 @@ class AccountManager {
.slice(0, 32);
const base = toToken(payload.model) || toToken(payload.serialNumber) || toToken(payload.assetName) || 'ASSET';
const now = new Date();
const timestamp = [
String(now.getFullYear()),
String(now.getMonth() + 1).padStart(2, '0'),
String(now.getDate()).padStart(2, '0'),
String(now.getHours()).padStart(2, '0'),
String(now.getMinutes()).padStart(2, '0'),
String(now.getSeconds()).padStart(2, '0'),
String(now.getMilliseconds()).padStart(3, '0')
].join('');
const timestamp = this.formatTimestampForCode(new Date(), true);
const randomSuffix = String(Math.floor(Math.random() * 100)).padStart(2, '0');
return `AST-${base}-${timestamp}${randomSuffix}`;
}
@@ -6481,8 +6528,7 @@ class AccountManager {
const workbook = window.XLSX.utils.book_new();
window.XLSX.utils.book_append_sheet(workbook, worksheet, 'TaiSan');
const now = new Date();
const timestamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
const timestamp = this.formatTimestampForCode(new Date()).slice(0, 8);
window.XLSX.writeFile(workbook, `danh-sach-tai-san-${timestamp}.xlsx`);
this.notifySuccess('Xuất Excel thành công');
}
@@ -7418,7 +7464,7 @@ class AccountManager {
if (Number.isNaN(date.getTime())) {
return String(value);
}
return date.toLocaleString();
return APP_DATE_TIME_FORMATTER.format(date);
}
// ========== Users Management ==========