const crypto = require('crypto'); const { sql, getPool } = require('./db'); const PASSWORD_HASH_PREFIX = 'pbkdf2'; const PASSWORD_HASH_ITERATIONS = 120000; const PASSWORD_HASH_KEY_LENGTH = 32; const PASSWORD_HASH_DIGEST = 'sha256'; const PLACEHOLDER_PASSWORD_HASH = 'ui-placeholder-password-hash'; const EMAIL_CONFIRMATION_EXPIRES_MS = Number(process.env.EMAIL_CONFIRMATION_EXPIRES_MS || 1000 * 60 * 60 * 24); let emailConfirmationSchemaPromise; function toDateInputValue(value) { const date = value ? new Date(value) : new Date(); if (Number.isNaN(date.getTime())) return new Date().toISOString().slice(0, 10); return date.toISOString().slice(0, 10); } function formatDate(value) { if (!value) return ''; return toDateInputValue(value); } function formatFileSize(bytes) { if (!Number.isFinite(Number(bytes)) || Number(bytes) <= 0) return ''; const numericBytes = Number(bytes); const units = ['B', 'KB', 'MB', 'GB']; const index = Math.min(Math.floor(Math.log(numericBytes) / Math.log(1024)), units.length - 1); const value = numericBytes / Math.pow(1024, index); return `${value.toFixed(value >= 10 || index === 0 ? 0 : 1)} ${units[index]}`; } function normalizeRole(role) { return role === 'Admin' ? 'Admin' : 'User'; } function normalizeEmail(email) { return String(email || '').trim().toLowerCase(); } function isPasswordHashUsable(passwordHash) { return String(passwordHash || '').startsWith(`${PASSWORD_HASH_PREFIX}$`); } function hashPassword(password) { const salt = crypto.randomBytes(16).toString('base64url'); const hash = crypto.pbkdf2Sync( password, salt, PASSWORD_HASH_ITERATIONS, PASSWORD_HASH_KEY_LENGTH, PASSWORD_HASH_DIGEST ).toString('base64url'); return `${PASSWORD_HASH_PREFIX}$${PASSWORD_HASH_DIGEST}$${PASSWORD_HASH_ITERATIONS}$${salt}$${hash}`; } function verifyPassword(password, storedHash) { if (!isPasswordHashUsable(storedHash)) return false; const [prefix, digest, iterationsText, salt, expectedHash] = String(storedHash).split('$'); const iterations = Number(iterationsText); if (prefix !== PASSWORD_HASH_PREFIX || !digest || !iterations || !salt || !expectedHash) { return false; } try { const actualHash = crypto.pbkdf2Sync( password, salt, iterations, Buffer.from(expectedHash, 'base64url').length, digest ).toString('base64url'); const actualBuffer = Buffer.from(actualHash); const expectedBuffer = Buffer.from(expectedHash); return actualBuffer.length === expectedBuffer.length && crypto.timingSafeEqual(actualBuffer, expectedBuffer); } catch (error) { return false; } } function mapCurrentUserRow(row) { if (!row) return null; return { id: String(row.Id), username: row.Username, email: row.Email, name: row.FullName || row.Username, fullName: row.FullName || '', role: row.Role, isActive: Boolean(row.IsActive) }; } function mapUserRow(row) { return { ...mapCurrentUserRow(row), status: row.IsActive ? 'Active' : 'Inactive', createdAt: formatDate(row.CreatedAt), updatedAt: formatDate(row.UpdatedAt), packageCount: Number(row.PackageCount || 0), applicationCount: Number(row.ApplicationCount || 0) }; } function duplicateUserError() { const error = new Error('Username or email already exists.'); error.code = 'DUPLICATE_USER'; return error; } function duplicatePackageError() { const error = new Error('Package code or version already exists.'); error.code = 'DUPLICATE_PACKAGE'; return error; } function duplicateApplicationError() { const error = new Error('Application code already exists.'); error.code = 'DUPLICATE_APPLICATION'; return error; } function userHasOwnedDataError() { const error = new Error('User owns packages or applications.'); error.code = 'USER_HAS_OWNED_DATA'; return error; } function hashEmailToken(token) { return crypto.createHash('sha256').update(String(token || '')).digest('hex'); } async function ensureEmailConfirmationSchema() { if (!emailConfirmationSchemaPromise) { emailConfirmationSchemaPromise = getPool().then((pool) => pool.request().query(` IF OBJECT_ID(N'dbo.EmailConfirmationTokens', N'U') IS NULL BEGIN CREATE TABLE dbo.EmailConfirmationTokens ( Id UNIQUEIDENTIFIER NOT NULL CONSTRAINT PK_EmailConfirmationTokens PRIMARY KEY CLUSTERED CONSTRAINT DF_EmailConfirmationTokens_Id DEFAULT NEWSEQUENTIALID(), UserId UNIQUEIDENTIFIER NOT NULL, TokenHash CHAR(64) NOT NULL, ExpiresAt DATETIME2(3) NOT NULL, ConfirmedAt DATETIME2(3) NULL, CreatedAt DATETIME2(3) NOT NULL CONSTRAINT DF_EmailConfirmationTokens_CreatedAt DEFAULT SYSUTCDATETIME(), CONSTRAINT FK_EmailConfirmationTokens_User FOREIGN KEY (UserId) REFERENCES dbo.Users(Id) ON DELETE CASCADE ); END; IF NOT EXISTS ( SELECT 1 FROM sys.indexes WHERE name = N'UX_EmailConfirmationTokens_TokenHash' AND object_id = OBJECT_ID(N'dbo.EmailConfirmationTokens') ) BEGIN CREATE UNIQUE INDEX UX_EmailConfirmationTokens_TokenHash ON dbo.EmailConfirmationTokens(TokenHash); END; IF NOT EXISTS ( SELECT 1 FROM sys.indexes WHERE name = N'IX_EmailConfirmationTokens_UserId' AND object_id = OBJECT_ID(N'dbo.EmailConfirmationTokens') ) BEGIN CREATE INDEX IX_EmailConfirmationTokens_UserId ON dbo.EmailConfirmationTokens(UserId); END; `)); } return emailConfirmationSchemaPromise; } function normalizePackageStatus(isActive) { return isActive ? 'Active' : 'Archived'; } function normalizeApplicationStatus(status) { return ['Draft', 'Released', 'Archived'].includes(status) ? status : 'Draft'; } function normalizeVersionStatus(row) { if (row.IsLatest) return 'Latest'; if (row.IsDeprecated) return 'Deprecated'; return 'Stable'; } function mapPackageRow(row) { return { id: String(row.Id), code: row.PackageCode, name: row.PackageName, type: row.PackageType, latestVersion: row.LatestVersion || '', latestReleaseDate: formatDate(row.LatestReleaseDate), status: normalizePackageStatus(row.IsActive), owner: row.CreatedByUsername || '', description: row.Description || '', artifact: row.LatestFilePath || row.LatestDockerImage || '', versionCount: Number(row.VersionCount || 0), versions: [] }; } function mapVersionRow(row) { return { id: String(row.Id), packageId: String(row.PackageId), version: row.Version, releaseDate: formatDate(row.ReleaseDate), uploadedBy: row.UploadedBy || '', status: normalizeVersionStatus(row), size: formatFileSize(row.FileSizeBytes), changeLog: row.ChangeLog || '', filePath: row.FilePath || '', dockerImage: row.DockerImage || '', checksum: row.FileChecksumSha256 || '' }; } function mapApplicationRow(row) { return { id: String(row.Id), code: row.AppCode, name: row.AppName, version: row.AppVersion, status: row.Status, createdAt: formatDate(row.CreatedAt), createdBy: row.CreatedByUsername || '', notes: row.Notes || row.Description || '', packageCount: Number(row.PackageCount || 0), packages: [] }; } function mapApplicationPackageRow(row) { return { id: String(row.Id), applicationId: String(row.ApplicationId), packageId: String(row.PackageId), code: row.PackageCode, name: row.PackageName, type: row.PackageType, selectedVersionId: row.SelectedVersionId ? String(row.SelectedVersionId) : '', selectedVersion: row.SelectedVersion || '', filePath: row.FilePath || '', dockerImage: row.DockerImage || '' }; } function toAbsoluteUrl(baseUrl, filePath) { if (!filePath) return ''; if (/^https?:\/\//i.test(filePath)) return filePath; const normalizedBaseUrl = String(baseUrl || '').replace(/\/+$/, ''); const normalizedPath = String(filePath).startsWith('/') ? filePath : `/${filePath}`; return `${normalizedBaseUrl}${normalizedPath}`; } async function getUserById(id) { const pool = await getPool(); const result = await pool.request() .input('Id', sql.NVarChar(100), id) .query(` SELECT TOP (1) Id, Username, Email, FullName, Role, IsActive FROM dbo.Users WHERE CONVERT(NVARCHAR(36), Id) = @Id; `); return mapCurrentUserRow(result.recordset[0]); } async function getUserOwnershipCounts(userId) { const pool = await getPool(); const result = await pool.request() .input('UserId', sql.UniqueIdentifier, userId) .query(` SELECT (SELECT COUNT_BIG(*) FROM dbo.Packages WHERE CreatedByUserId = @UserId) AS PackageCount, (SELECT COUNT_BIG(*) FROM dbo.Applications WHERE CreatedByUserId = @UserId) AS ApplicationCount; `); const row = result.recordset[0]; return { packageCount: Number(row.PackageCount || 0), applicationCount: Number(row.ApplicationCount || 0) }; } async function listUsers() { const pool = await getPool(); const result = await pool.request().query(` SELECT u.Id, u.Username, u.Email, u.FullName, u.Role, u.IsActive, u.CreatedAt, u.UpdatedAt, package_count.PackageCount, application_count.ApplicationCount FROM dbo.Users AS u OUTER APPLY ( SELECT COUNT_BIG(*) AS PackageCount FROM dbo.Packages AS p WHERE p.CreatedByUserId = u.Id ) AS package_count OUTER APPLY ( SELECT COUNT_BIG(*) AS ApplicationCount FROM dbo.Applications AS a WHERE a.CreatedByUserId = u.Id ) AS application_count ORDER BY u.CreatedAt DESC, u.Username ASC; `); return result.recordset.map(mapUserRow); } async function countUsableUsers() { const pool = await getPool(); const result = await pool.request() .input('PasswordHashPrefix', sql.NVarChar(50), `${PASSWORD_HASH_PREFIX}$%`) .query(` SELECT COUNT_BIG(*) AS Total FROM dbo.Users WHERE PasswordHash LIKE @PasswordHashPrefix; `); return Number(result.recordset[0].Total || 0); } async function findUserAccountByIdentifier(identifier) { const pool = await getPool(); const result = await pool.request() .input('Identifier', sql.NVarChar(255), String(identifier || '').trim()) .query(` SELECT TOP (1) Id, Username, Email, PasswordHash, FullName, Role, IsActive FROM dbo.Users WHERE Username = @Identifier OR Email = @Identifier; `); return result.recordset[0] || null; } async function getPendingUserByEmail(email) { const pool = await getPool(); const result = await pool.request() .input('Email', sql.NVarChar(255), normalizeEmail(email)) .input('PasswordHashPrefix', sql.NVarChar(50), `${PASSWORD_HASH_PREFIX}$%`) .query(` SELECT TOP (1) Id, Username, Email, FullName, Role, IsActive FROM dbo.Users WHERE Email = @Email AND IsActive = 0 AND PasswordHash LIKE @PasswordHashPrefix; `); return mapCurrentUserRow(result.recordset[0]); } async function getRegistrationConflict({ username, email }) { const pool = await getPool(); const result = await pool.request() .input('Username', sql.NVarChar(100), String(username || '').trim()) .input('Email', sql.NVarChar(255), normalizeEmail(email)) .query(` SELECT TOP (1) Id, Username, Email, FullName, Role, IsActive FROM dbo.Users WHERE Username = @Username OR Email = @Email ORDER BY CASE WHEN Email = @Email THEN 0 WHEN Username = @Username THEN 1 ELSE 2 END, CreatedAt ASC; `); const user = mapCurrentUserRow(result.recordset[0]); if (!user) return null; return { user, field: normalizeEmail(user.email) === normalizeEmail(email) ? 'email' : 'username' }; } async function authenticateUser(identifier, password) { const row = await findUserAccountByIdentifier(identifier); if (!row || !row.IsActive) return null; if (!verifyPassword(String(password || ''), row.PasswordHash)) return null; return mapCurrentUserRow(row); } async function createUser(input) { const pool = await getPool(); const username = String(input.username || '').trim(); const email = normalizeEmail(input.email); const fullName = String(input.fullName || '').trim() || null; const passwordHash = hashPassword(String(input.password || '')); const usableUserCount = await countUsableUsers(); const role = input.role ? normalizeRole(input.role) : (usableUserCount === 0 ? 'Admin' : 'User'); const isActive = input.isActive === undefined ? true : Boolean(input.isActive); const existingResult = await pool.request() .input('Username', sql.NVarChar(100), username) .input('Email', sql.NVarChar(255), email) .query(` SELECT TOP (1) Id, PasswordHash FROM dbo.Users WHERE Username = @Username OR Email = @Email; `); const existing = existingResult.recordset[0]; if (existing) { if (usableUserCount === 0 && existing.PasswordHash === PLACEHOLDER_PASSWORD_HASH) { const updateResult = await pool.request() .input('Id', sql.UniqueIdentifier, existing.Id) .input('Username', sql.NVarChar(100), username) .input('Email', sql.NVarChar(255), email) .input('PasswordHash', sql.NVarChar(500), passwordHash) .input('FullName', sql.NVarChar(200), fullName) .input('Role', sql.NVarChar(50), role) .input('IsActive', sql.Bit, isActive ? 1 : 0) .query(` UPDATE dbo.Users SET Username = @Username, Email = @Email, PasswordHash = @PasswordHash, FullName = @FullName, Role = @Role, IsActive = @IsActive, UpdatedAt = SYSUTCDATETIME() OUTPUT inserted.Id, inserted.Username, inserted.Email, inserted.FullName, inserted.Role, inserted.IsActive WHERE Id = @Id; `); return mapCurrentUserRow(updateResult.recordset[0]); } throw duplicateUserError(); } if (usableUserCount === 0) { const placeholderResult = await pool.request() .input('PasswordHash', sql.NVarChar(500), PLACEHOLDER_PASSWORD_HASH) .query(` SELECT TOP (1) Id FROM dbo.Users WHERE PasswordHash = @PasswordHash ORDER BY CreatedAt ASC; `); const placeholder = placeholderResult.recordset[0]; if (placeholder) { const updateResult = await pool.request() .input('Id', sql.UniqueIdentifier, placeholder.Id) .input('Username', sql.NVarChar(100), username) .input('Email', sql.NVarChar(255), email) .input('PasswordHash', sql.NVarChar(500), passwordHash) .input('FullName', sql.NVarChar(200), fullName) .input('Role', sql.NVarChar(50), role) .input('IsActive', sql.Bit, isActive ? 1 : 0) .query(` UPDATE dbo.Users SET Username = @Username, Email = @Email, PasswordHash = @PasswordHash, FullName = @FullName, Role = @Role, IsActive = @IsActive, UpdatedAt = SYSUTCDATETIME() OUTPUT inserted.Id, inserted.Username, inserted.Email, inserted.FullName, inserted.Role, inserted.IsActive WHERE Id = @Id; `); return mapCurrentUserRow(updateResult.recordset[0]); } } try { const result = await pool.request() .input('Username', sql.NVarChar(100), username) .input('Email', sql.NVarChar(255), email) .input('PasswordHash', sql.NVarChar(500), passwordHash) .input('FullName', sql.NVarChar(200), fullName) .input('Role', sql.NVarChar(50), role) .input('IsActive', sql.Bit, isActive ? 1 : 0) .query(` INSERT dbo.Users (Username, Email, PasswordHash, FullName, Role, IsActive) OUTPUT inserted.Id, inserted.Username, inserted.Email, inserted.FullName, inserted.Role, inserted.IsActive VALUES (@Username, @Email, @PasswordHash, @FullName, @Role, @IsActive); `); return mapCurrentUserRow(result.recordset[0]); } catch (error) { if (error.number === 2601 || error.number === 2627) { throw duplicateUserError(); } throw error; } } async function createEmailConfirmationToken(userId) { await ensureEmailConfirmationSchema(); const pool = await getPool(); const token = crypto.randomBytes(32).toString('base64url'); const tokenHash = hashEmailToken(token); const expiresAt = new Date(Date.now() + EMAIL_CONFIRMATION_EXPIRES_MS); await pool.request() .input('UserId', sql.UniqueIdentifier, userId) .input('TokenHash', sql.Char(64), tokenHash) .input('ExpiresAt', sql.DateTime2, expiresAt) .query(` DELETE FROM dbo.EmailConfirmationTokens WHERE UserId = @UserId AND ConfirmedAt IS NULL; INSERT dbo.EmailConfirmationTokens (UserId, TokenHash, ExpiresAt) VALUES (@UserId, @TokenHash, @ExpiresAt); `); return token; } async function confirmEmailToken(token) { await ensureEmailConfirmationSchema(); const pool = await getPool(); const result = await pool.request() .input('TokenHash', sql.Char(64), hashEmailToken(token)) .query(` SET NOCOUNT ON; SET XACT_ABORT ON; BEGIN TRANSACTION; DECLARE @TokenId UNIQUEIDENTIFIER; DECLARE @UserId UNIQUEIDENTIFIER; SELECT TOP (1) @TokenId = Id, @UserId = UserId FROM dbo.EmailConfirmationTokens WITH (UPDLOCK, HOLDLOCK) WHERE TokenHash = @TokenHash AND ConfirmedAt IS NULL AND ExpiresAt >= SYSUTCDATETIME(); IF @TokenId IS NULL BEGIN COMMIT TRANSACTION; SELECT TOP (0) Id, Username, Email, FullName, Role, IsActive FROM dbo.Users; RETURN; END; UPDATE dbo.EmailConfirmationTokens SET ConfirmedAt = SYSUTCDATETIME() WHERE Id = @TokenId; UPDATE dbo.Users SET IsActive = 1, UpdatedAt = SYSUTCDATETIME() OUTPUT inserted.Id, inserted.Username, inserted.Email, inserted.FullName, inserted.Role, inserted.IsActive WHERE Id = @UserId; COMMIT TRANSACTION; `); return mapCurrentUserRow(result.recordset[0]); } async function updateUserAccess(input) { const pool = await getPool(); await pool.request() .input('Id', sql.UniqueIdentifier, input.userId) .input('Role', sql.NVarChar(50), normalizeRole(input.role)) .input('IsActive', sql.Bit, input.isActive ? 1 : 0) .query(` UPDATE dbo.Users SET Role = @Role, IsActive = @IsActive, UpdatedAt = SYSUTCDATETIME() WHERE Id = @Id; `); } async function updateUser(input) { const pool = await getPool(); const username = String(input.username || '').trim(); const email = normalizeEmail(input.email); const fullName = String(input.fullName || '').trim() || null; const role = normalizeRole(input.role); const isActive = input.isActive ? 1 : 0; const passwordHash = input.password ? hashPassword(String(input.password)) : null; const existingResult = await pool.request() .input('Id', sql.UniqueIdentifier, input.userId) .input('Username', sql.NVarChar(100), username) .input('Email', sql.NVarChar(255), email) .query(` SELECT TOP (1) Id FROM dbo.Users WHERE (Username = @Username OR Email = @Email) AND Id <> @Id; `); if (existingResult.recordset.length > 0) { throw duplicateUserError(); } const result = await pool.request() .input('Id', sql.UniqueIdentifier, input.userId) .input('Username', sql.NVarChar(100), username) .input('Email', sql.NVarChar(255), email) .input('FullName', sql.NVarChar(200), fullName) .input('Role', sql.NVarChar(50), role) .input('IsActive', sql.Bit, isActive) .input('PasswordHash', sql.NVarChar(500), passwordHash) .query(` UPDATE dbo.Users SET Username = @Username, Email = @Email, FullName = @FullName, Role = @Role, IsActive = @IsActive, PasswordHash = COALESCE(@PasswordHash, PasswordHash), UpdatedAt = SYSUTCDATETIME() OUTPUT inserted.Id, inserted.Username, inserted.Email, inserted.FullName, inserted.Role, inserted.IsActive WHERE Id = @Id; `); return mapCurrentUserRow(result.recordset[0]); } async function deleteUser(userId) { const counts = await getUserOwnershipCounts(userId); if (counts.packageCount > 0 || counts.applicationCount > 0) { throw userHasOwnedDataError(); } const pool = await getPool(); const result = await pool.request() .input('Id', sql.UniqueIdentifier, userId) .query(` DELETE FROM dbo.Users OUTPUT deleted.Id WHERE Id = @Id; `); return result.recordset.length > 0; } async function listPackages() { const pool = await getPool(); const result = await pool.request().query(` SELECT * FROM dbo.vw_PackageList ORDER BY CreatedAt DESC, PackageName ASC; `); return result.recordset.map(mapPackageRow); } async function listPackageVersions() { const pool = await getPool(); const result = await pool.request().query(` SELECT * FROM dbo.vw_PackageVersionList ORDER BY PackageId, IsLatest DESC, ReleaseDate DESC, UploadedAt DESC; `); return result.recordset.map(mapVersionRow); } async function attachPackageVersions(packages) { if (packages.length === 0) return packages; const versions = await listPackageVersions(); const versionsByPackageId = new Map(); versions.forEach((version) => { if (!versionsByPackageId.has(version.packageId)) { versionsByPackageId.set(version.packageId, []); } versionsByPackageId.get(version.packageId).push(version); }); return packages.map((packageItem) => ({ ...packageItem, versions: versionsByPackageId.get(packageItem.id) || [] })); } async function getPackageById(id) { const pool = await getPool(); const packageResult = await pool.request() .input('Id', sql.NVarChar(100), id) .query(` SELECT TOP (1) * FROM dbo.vw_PackageList WHERE CONVERT(NVARCHAR(36), Id) = @Id OR PackageCode = @Id; `); if (packageResult.recordset.length === 0) return null; const packageItem = mapPackageRow(packageResult.recordset[0]); const versions = await pool.request() .input('PackageId', sql.UniqueIdentifier, packageItem.id) .query(` SELECT * FROM dbo.vw_PackageVersionList WHERE PackageId = @PackageId ORDER BY IsLatest DESC, ReleaseDate DESC, UploadedAt DESC; `); packageItem.versions = versions.recordset.map(mapVersionRow); return packageItem; } async function listApplications() { const pool = await getPool(); const result = await pool.request().query(` SELECT * FROM dbo.vw_ApplicationList ORDER BY CreatedAt DESC, AppName ASC; `); const applications = result.recordset.map(mapApplicationRow); return attachApplicationPackages(applications); } async function listApplicationPackages(applicationId) { const pool = await getPool(); const request = pool.request(); let whereClause = ''; if (applicationId) { request.input('ApplicationId', sql.UniqueIdentifier, applicationId); whereClause = 'WHERE ApplicationId = @ApplicationId'; } const result = await request.query(` SELECT * FROM dbo.vw_ApplicationPackageDetails ${whereClause} ORDER BY AppName ASC, PackageName ASC; `); return result.recordset.map(mapApplicationPackageRow); } async function attachApplicationPackages(applications) { if (applications.length === 0) return applications; const applicationPackages = await listApplicationPackages(); const packagesByApplicationId = new Map(); applicationPackages.forEach((packageItem) => { if (!packagesByApplicationId.has(packageItem.applicationId)) { packagesByApplicationId.set(packageItem.applicationId, []); } packagesByApplicationId.get(packageItem.applicationId).push(packageItem); }); return applications.map((application) => { const packages = packagesByApplicationId.get(application.id) || []; return { ...application, packageCount: packages.length, packages }; }); } async function getApplicationById(id) { const pool = await getPool(); const appResult = await pool.request() .input('Id', sql.NVarChar(100), id) .query(` SELECT TOP (1) * FROM dbo.vw_ApplicationList WHERE CONVERT(NVARCHAR(36), Id) = @Id OR AppCode = @Id; `); if (appResult.recordset.length === 0) return null; const application = mapApplicationRow(appResult.recordset[0]); application.packages = await listApplicationPackages(application.id); application.packageCount = application.packages.length; return application; } async function getApplicationManifest(appCode, version, baseUrl) { const pool = await getPool(); const appResult = await pool.request() .input('AppCode', sql.NVarChar(100), String(appCode || '').trim()) .input('AppVersion', sql.NVarChar(50), String(version || '').trim()) .query(` SELECT TOP (1) Id, AppCode, AppName, AppVersion FROM dbo.Applications WHERE AppCode = @AppCode AND AppVersion = @AppVersion AND Status = N'Released'; `); const appRow = appResult.recordset[0]; if (!appRow) return null; const componentResult = await pool.request() .input('ApplicationId', sql.UniqueIdentifier, appRow.Id) .query(` SELECT ap.Id, p.PackageCode, p.PackageName, p.PackageType, COALESCE(selected_version.Version, latest_version.Version) AS Version, COALESCE(selected_version.FilePath, latest_version.FilePath) AS FilePath, COALESCE(selected_version.DockerImage, latest_version.DockerImage) AS DockerImage, COALESCE(selected_version.FileChecksumSha256, latest_version.FileChecksumSha256) AS FileChecksumSha256, ROW_NUMBER() OVER (ORDER BY ap.AddedAt ASC, p.PackageCode ASC) * 10 AS InstallOrder FROM dbo.ApplicationPackages AS ap INNER JOIN dbo.Packages AS p ON p.Id = ap.PackageId LEFT JOIN dbo.PackageVersions AS selected_version ON selected_version.Id = ap.SelectedVersionId OUTER APPLY ( SELECT TOP (1) latest.* FROM dbo.PackageVersions AS latest WHERE latest.PackageId = p.Id AND latest.IsLatest = 1 ORDER BY latest.ReleaseDate DESC, latest.UploadedAt DESC ) AS latest_version WHERE ap.ApplicationId = @ApplicationId ORDER BY ap.AddedAt ASC, p.PackageCode ASC; `); const components = componentResult.recordset.map((row) => { const installOrder = Number(row.InstallOrder || 10); if (row.PackageType === 'docker') { return { componentId: row.PackageCode, type: 'docker', installOrder, required: true, image: row.DockerImage || '', tag: row.Version || 'latest', containerName: row.PackageCode }; } return { componentId: row.PackageCode, type: 'deb', installOrder, required: true, packageName: row.PackageCode, version: row.Version || '', downloadUrl: toAbsoluteUrl(baseUrl, row.FilePath), sha256: row.FileChecksumSha256 || '' }; }); return { schemaVersion: '1.0', appId: appRow.AppCode, appName: appRow.AppName, version: appRow.AppVersion, architecture: 'amd64', components }; } async function getStats() { const pool = await getPool(); const result = await pool.request().query(` SELECT (SELECT COUNT_BIG(*) FROM dbo.Packages) AS TotalPackages, (SELECT COUNT_BIG(*) FROM dbo.Packages WHERE IsActive = 1) AS ActivePackages, (SELECT COUNT_BIG(*) FROM dbo.PackageVersions) AS TotalVersions, (SELECT COUNT_BIG(*) FROM dbo.Applications) AS TotalApplications, (SELECT COUNT_BIG(*) FROM dbo.Applications WHERE Status = N'Released') AS ReleasedApplications; `); const row = result.recordset[0]; return { totalPackages: Number(row.TotalPackages || 0), activePackages: Number(row.ActivePackages || 0), totalVersions: Number(row.TotalVersions || 0), totalApplications: Number(row.TotalApplications || 0), releasedApplications: Number(row.ReleasedApplications || 0) }; } async function getActivity() { const pool = await getPool(); const result = await pool.request().query(` SELECT TOP (6) p.PackageName, p.PackageCode, pv.Version, pv.UploadedAt, pv.IsLatest FROM dbo.PackageVersions AS pv INNER JOIN dbo.Packages AS p ON p.Id = pv.PackageId ORDER BY pv.UploadedAt DESC; `); return result.recordset.map((row) => ({ title: `${row.IsLatest ? 'Latest' : 'Upload'} ${row.Version}`, detail: `${row.PackageName} (${row.PackageCode})`, time: formatDate(row.UploadedAt), icon: row.IsLatest ? 'stars' : 'upload_file' })); } async function createPackageWithVersion(input) { const pool = await getPool(); const transaction = new sql.Transaction(pool); await transaction.begin(); try { const packageResult = await new sql.Request(transaction) .input('PackageCode', sql.NVarChar(100), input.packageCode) .input('PackageName', sql.NVarChar(200), input.packageName) .input('PackageType', sql.NVarChar(20), input.packageType) .input('Description', sql.NVarChar(sql.MAX), input.description || null) .input('CreatedByUserId', sql.UniqueIdentifier, input.createdByUserId) .query(` INSERT dbo.Packages (PackageCode, PackageName, PackageType, Description, CreatedByUserId) OUTPUT inserted.Id VALUES (@PackageCode, @PackageName, @PackageType, @Description, @CreatedByUserId); `); const packageId = String(packageResult.recordset[0].Id); const versionResult = await new sql.Request(transaction) .input('PackageId', sql.UniqueIdentifier, packageId) .input('Version', sql.NVarChar(50), input.version) .input('FilePath', sql.NVarChar(1000), input.filePath || null) .input('DockerImage', sql.NVarChar(500), input.dockerImage || null) .input('FileChecksumSha256', sql.Char(64), input.checksum || null) .input('FileSizeBytes', sql.BigInt, input.fileSizeBytes || null) .input('ChangeLog', sql.NVarChar(sql.MAX), input.changeLog || null) .input('ReleaseDate', sql.DateTime2, input.releaseDate ? new Date(input.releaseDate) : new Date()) .query(` INSERT dbo.PackageVersions ( PackageId, Version, FilePath, DockerImage, FileChecksumSha256, FileSizeBytes, ChangeLog, ReleaseDate, IsLatest ) OUTPUT inserted.Id VALUES ( @PackageId, @Version, @FilePath, @DockerImage, @FileChecksumSha256, @FileSizeBytes, @ChangeLog, @ReleaseDate, 1 ); `); await transaction.commit(); return { packageId, versionId: String(versionResult.recordset[0].Id) }; } catch (error) { await transaction.rollback(); if (error.number === 2601 || error.number === 2627) { throw duplicatePackageError(); } throw error; } } async function addPackageVersion(input) { const pool = await getPool(); let insertResult; try { insertResult = await pool.request() .input('PackageId', sql.UniqueIdentifier, input.packageId) .input('Version', sql.NVarChar(50), input.version) .input('FilePath', sql.NVarChar(1000), input.filePath || null) .input('DockerImage', sql.NVarChar(500), input.dockerImage || null) .input('FileChecksumSha256', sql.Char(64), input.checksum || null) .input('FileSizeBytes', sql.BigInt, input.fileSizeBytes || null) .input('ChangeLog', sql.NVarChar(sql.MAX), input.changeLog || null) .input('ReleaseDate', sql.DateTime2, input.releaseDate ? new Date(input.releaseDate) : new Date()) .query(` INSERT dbo.PackageVersions ( PackageId, Version, FilePath, DockerImage, FileChecksumSha256, FileSizeBytes, ChangeLog, ReleaseDate ) OUTPUT inserted.Id VALUES ( @PackageId, @Version, @FilePath, @DockerImage, @FileChecksumSha256, @FileSizeBytes, @ChangeLog, @ReleaseDate ); `); } catch (error) { if (error.number === 2601 || error.number === 2627) { throw duplicatePackageError(); } throw error; } const versionId = String(insertResult.recordset[0].Id); await pool.request() .input('PackageVersionId', sql.UniqueIdentifier, versionId) .execute('dbo.SetLatestPackageVersion'); return versionId; } async function deletePackage(packageId) { const pool = await getPool(); const transaction = new sql.Transaction(pool); await transaction.begin(); try { await new sql.Request(transaction) .input('Id', sql.UniqueIdentifier, packageId) .query(` DELETE FROM dbo.ApplicationPackages WHERE PackageId = @Id; `); const result = await new sql.Request(transaction) .input('Id', sql.UniqueIdentifier, packageId) .query(` DELETE FROM dbo.Packages OUTPUT deleted.Id WHERE Id = @Id; `); await transaction.commit(); return result.recordset.length > 0; } catch (error) { await transaction.rollback(); throw error; } } async function setLatestPackageVersion(packageVersionId) { const pool = await getPool(); await pool.request() .input('PackageVersionId', sql.UniqueIdentifier, packageVersionId) .execute('dbo.SetLatestPackageVersion'); } async function deletePackageVersion(packageVersionId) { const pool = await getPool(); const versionResult = await pool.request() .input('Id', sql.UniqueIdentifier, packageVersionId) .query(` SELECT TOP (1) PackageId FROM dbo.PackageVersions WHERE Id = @Id; `); const version = versionResult.recordset[0]; if (!version) { return { deleted: false, packageId: null }; } await pool.request() .input('PackageVersionId', sql.UniqueIdentifier, packageVersionId) .execute('dbo.DeletePackageVersion'); return { deleted: true, packageId: String(version.PackageId) }; } async function createApplication(input) { const pool = await getPool(); const transaction = new sql.Transaction(pool); await transaction.begin(); try { const appResult = await new sql.Request(transaction) .input('AppCode', sql.NVarChar(100), input.appCode) .input('AppName', sql.NVarChar(200), input.appName) .input('AppVersion', sql.NVarChar(50), input.appVersion) .input('Notes', sql.NVarChar(500), input.notes || null) .input('CreatedByUserId', sql.UniqueIdentifier, input.createdByUserId) .input('Status', sql.NVarChar(50), normalizeApplicationStatus(input.status)) .query(` INSERT dbo.Applications (AppCode, AppName, AppVersion, Notes, CreatedByUserId, Status) OUTPUT inserted.Id VALUES (@AppCode, @AppName, @AppVersion, @Notes, @CreatedByUserId, @Status); `); const applicationId = String(appResult.recordset[0].Id); for (const packageItem of input.packages) { await new sql.Request(transaction) .input('ApplicationId', sql.UniqueIdentifier, applicationId) .input('PackageId', sql.UniqueIdentifier, packageItem.packageId) .input('SelectedVersionId', sql.UniqueIdentifier, packageItem.selectedVersionId || null) .query(` INSERT dbo.ApplicationPackages (ApplicationId, PackageId, SelectedVersionId) VALUES (@ApplicationId, @PackageId, @SelectedVersionId); `); } await transaction.commit(); return applicationId; } catch (error) { await transaction.rollback(); if (error.number === 2601 || error.number === 2627) { throw duplicateApplicationError(); } throw error; } } async function updateApplication(input) { const pool = await getPool(); const transaction = new sql.Transaction(pool); await transaction.begin(); try { const conflictResult = await new sql.Request(transaction) .input('ApplicationId', sql.UniqueIdentifier, input.applicationId) .input('AppCode', sql.NVarChar(100), input.appCode) .query(` SELECT TOP (1) Id FROM dbo.Applications WHERE AppCode = @AppCode AND Id <> @ApplicationId; `); if (conflictResult.recordset.length > 0) { throw duplicateApplicationError(); } const appResult = await new sql.Request(transaction) .input('ApplicationId', sql.UniqueIdentifier, input.applicationId) .input('AppCode', sql.NVarChar(100), input.appCode) .input('AppName', sql.NVarChar(200), input.appName) .input('AppVersion', sql.NVarChar(50), input.appVersion) .input('Notes', sql.NVarChar(500), input.notes || null) .input('Status', sql.NVarChar(50), normalizeApplicationStatus(input.status)) .query(` UPDATE dbo.Applications SET AppCode = @AppCode, AppName = @AppName, AppVersion = @AppVersion, Notes = @Notes, Status = @Status, UpdatedAt = SYSUTCDATETIME() OUTPUT inserted.Id WHERE Id = @ApplicationId; `); if (appResult.recordset.length === 0) { await transaction.commit(); return null; } await new sql.Request(transaction) .input('ApplicationId', sql.UniqueIdentifier, input.applicationId) .query(` DELETE FROM dbo.ApplicationPackages WHERE ApplicationId = @ApplicationId; `); for (const packageItem of input.packages) { await new sql.Request(transaction) .input('ApplicationId', sql.UniqueIdentifier, input.applicationId) .input('PackageId', sql.UniqueIdentifier, packageItem.packageId) .input('SelectedVersionId', sql.UniqueIdentifier, packageItem.selectedVersionId || null) .query(` INSERT dbo.ApplicationPackages (ApplicationId, PackageId, SelectedVersionId) VALUES (@ApplicationId, @PackageId, @SelectedVersionId); `); } await transaction.commit(); } catch (error) { await transaction.rollback(); if (error.code === 'DUPLICATE_APPLICATION') { throw error; } if (error.number === 2601 || error.number === 2627) { throw duplicateApplicationError(); } throw error; } return getApplicationById(input.applicationId); } async function updateApplicationStatus(applicationId, status) { const pool = await getPool(); const result = await pool.request() .input('ApplicationId', sql.UniqueIdentifier, applicationId) .input('Status', sql.NVarChar(50), normalizeApplicationStatus(status)) .query(` UPDATE dbo.Applications SET Status = @Status, UpdatedAt = SYSUTCDATETIME() OUTPUT inserted.Id WHERE Id = @ApplicationId; `); return result.recordset.length > 0; } async function deleteApplication(applicationId) { const pool = await getPool(); const result = await pool.request() .input('ApplicationId', sql.UniqueIdentifier, applicationId) .query(` DELETE FROM dbo.Applications OUTPUT deleted.Id WHERE Id = @ApplicationId; `); return result.recordset.length > 0; } async function removeApplicationPackage(applicationId, packageId) { const pool = await getPool(); const result = await pool.request() .input('ApplicationId', sql.UniqueIdentifier, applicationId) .input('PackageId', sql.UniqueIdentifier, packageId) .query(` DELETE FROM dbo.ApplicationPackages OUTPUT deleted.Id WHERE ApplicationId = @ApplicationId AND PackageId = @PackageId; `); return result.recordset.length > 0; } async function getPageData(currentUser) { const [stats, packageRows, applications, activity] = await Promise.all([ getStats(), listPackages(), listApplications(), getActivity() ]); const packages = await attachPackageVersions(packageRows); return { currentUser, stats, packages, applications, activity }; } module.exports = { authenticateUser, confirmEmailToken, createEmailConfirmationToken, createUser, deleteUser, getUserById, getPendingUserByEmail, getRegistrationConflict, listUsers, updateUser, updateUserAccess, getPageData, listPackages, listApplications, getApplicationManifest, getPackageById, getApplicationById, createPackageWithVersion, addPackageVersion, deletePackage, setLatestPackageVersion, deletePackageVersion, createApplication, updateApplication, updateApplicationStatus, deleteApplication, removeApplicationPackage };