web server

This commit is contained in:
2026-05-20 14:10:25 +07:00
parent 5ade939ff9
commit 190d2418da
30 changed files with 8917 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
IF DB_ID(N'RobotInstaller') IS NULL
BEGIN
CREATE DATABASE [RobotInstaller];
END;
GO
USE [RobotInstaller];
GO
PRINT N'Database RobotInstaller is ready.';
GO

View File

@@ -0,0 +1,291 @@
USE [RobotInstaller];
GO
SET ANSI_NULLS ON;
SET QUOTED_IDENTIFIER ON;
GO
IF OBJECT_ID(N'dbo.ApplicationPackages', N'U') IS NOT NULL
OR OBJECT_ID(N'dbo.PackageVersions', N'U') IS NOT NULL
OR OBJECT_ID(N'dbo.Applications', N'U') IS NOT NULL
OR OBJECT_ID(N'dbo.Packages', N'U') IS NOT NULL
OR OBJECT_ID(N'dbo.EmailConfirmationTokens', N'U') IS NOT NULL
OR OBJECT_ID(N'dbo.Users', N'U') IS NOT NULL
BEGIN
THROW 50001, 'Schema tables already exist. Review, drop, or migrate existing tables before running 02_schema.sql.', 1;
END;
GO
CREATE TABLE dbo.Users
(
Id UNIQUEIDENTIFIER NOT NULL
CONSTRAINT PK_Users PRIMARY KEY CLUSTERED
CONSTRAINT DF_Users_Id DEFAULT NEWSEQUENTIALID(),
Username NVARCHAR(100) NOT NULL,
Email NVARCHAR(255) NOT NULL,
PasswordHash NVARCHAR(500) NOT NULL,
FullName NVARCHAR(200) NULL,
Role NVARCHAR(50) NOT NULL
CONSTRAINT DF_Users_Role DEFAULT N'User',
IsActive BIT NOT NULL
CONSTRAINT DF_Users_IsActive DEFAULT 1,
CreatedAt DATETIME2(3) NOT NULL
CONSTRAINT DF_Users_CreatedAt DEFAULT SYSUTCDATETIME(),
UpdatedAt DATETIME2(3) NULL,
CONSTRAINT CK_Users_Role CHECK (Role IN (N'Admin', N'User')),
CONSTRAINT CK_Users_Username_NotBlank CHECK (LEN(LTRIM(RTRIM(Username))) > 0),
CONSTRAINT CK_Users_Email_NotBlank CHECK (LEN(LTRIM(RTRIM(Email))) > 0)
);
GO
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
);
GO
CREATE TABLE dbo.Packages
(
Id UNIQUEIDENTIFIER NOT NULL
CONSTRAINT PK_Packages PRIMARY KEY CLUSTERED
CONSTRAINT DF_Packages_Id DEFAULT NEWSEQUENTIALID(),
PackageCode NVARCHAR(100) NOT NULL,
PackageName NVARCHAR(200) NOT NULL,
PackageType NVARCHAR(20) NOT NULL,
Description NVARCHAR(MAX) NULL,
CreatedByUserId UNIQUEIDENTIFIER NOT NULL,
CreatedAt DATETIME2(3) NOT NULL
CONSTRAINT DF_Packages_CreatedAt DEFAULT SYSUTCDATETIME(),
UpdatedAt DATETIME2(3) NULL,
IsActive BIT NOT NULL
CONSTRAINT DF_Packages_IsActive DEFAULT 1,
CONSTRAINT FK_Packages_CreatedByUser
FOREIGN KEY (CreatedByUserId) REFERENCES dbo.Users(Id),
CONSTRAINT CK_Packages_PackageType CHECK (PackageType IN (N'deb', N'docker')),
CONSTRAINT CK_Packages_PackageCode_NotBlank CHECK (LEN(LTRIM(RTRIM(PackageCode))) > 0),
CONSTRAINT CK_Packages_PackageName_NotBlank CHECK (LEN(LTRIM(RTRIM(PackageName))) > 0)
);
GO
CREATE TABLE dbo.PackageVersions
(
Id UNIQUEIDENTIFIER NOT NULL
CONSTRAINT PK_PackageVersions PRIMARY KEY CLUSTERED
CONSTRAINT DF_PackageVersions_Id DEFAULT NEWSEQUENTIALID(),
PackageId UNIQUEIDENTIFIER NOT NULL,
Version NVARCHAR(50) NOT NULL,
FilePath NVARCHAR(1000) NULL,
DockerImage NVARCHAR(500) NULL,
FileChecksumSha256 CHAR(64) NULL,
FileSizeBytes BIGINT NULL,
ChangeLog NVARCHAR(MAX) NULL,
ReleaseDate DATETIME2(3) NOT NULL
CONSTRAINT DF_PackageVersions_ReleaseDate DEFAULT SYSUTCDATETIME(),
UploadedAt DATETIME2(3) NOT NULL
CONSTRAINT DF_PackageVersions_UploadedAt DEFAULT SYSUTCDATETIME(),
IsLatest BIT NOT NULL
CONSTRAINT DF_PackageVersions_IsLatest DEFAULT 0,
IsDeprecated BIT NOT NULL
CONSTRAINT DF_PackageVersions_IsDeprecated DEFAULT 0,
CONSTRAINT FK_PackageVersions_Package
FOREIGN KEY (PackageId) REFERENCES dbo.Packages(Id) ON DELETE CASCADE,
CONSTRAINT CK_PackageVersions_Version_NotBlank CHECK (LEN(LTRIM(RTRIM(Version))) > 0),
CONSTRAINT CK_PackageVersions_FileSizeBytes CHECK (FileSizeBytes IS NULL OR FileSizeBytes >= 0),
CONSTRAINT CK_PackageVersions_FileChecksumSha256 CHECK (
FileChecksumSha256 IS NULL
OR FileChecksumSha256 NOT LIKE '%[^0-9A-Fa-f]%'
)
);
GO
CREATE TABLE dbo.Applications
(
Id UNIQUEIDENTIFIER NOT NULL
CONSTRAINT PK_Applications PRIMARY KEY CLUSTERED
CONSTRAINT DF_Applications_Id DEFAULT NEWSEQUENTIALID(),
AppCode NVARCHAR(100) NOT NULL,
AppName NVARCHAR(200) NOT NULL,
AppVersion NVARCHAR(50) NOT NULL
CONSTRAINT DF_Applications_AppVersion DEFAULT N'1.0.0',
Description NVARCHAR(MAX) NULL,
CreatedByUserId UNIQUEIDENTIFIER NOT NULL,
CreatedAt DATETIME2(3) NOT NULL
CONSTRAINT DF_Applications_CreatedAt DEFAULT SYSUTCDATETIME(),
UpdatedAt DATETIME2(3) NULL,
Status NVARCHAR(50) NOT NULL
CONSTRAINT DF_Applications_Status DEFAULT N'Draft',
Notes NVARCHAR(500) NULL,
CONSTRAINT FK_Applications_CreatedByUser
FOREIGN KEY (CreatedByUserId) REFERENCES dbo.Users(Id),
CONSTRAINT CK_Applications_Status CHECK (Status IN (N'Draft', N'Released', N'Archived')),
CONSTRAINT CK_Applications_AppCode_NotBlank CHECK (LEN(LTRIM(RTRIM(AppCode))) > 0),
CONSTRAINT CK_Applications_AppName_NotBlank CHECK (LEN(LTRIM(RTRIM(AppName))) > 0),
CONSTRAINT CK_Applications_AppVersion_NotBlank CHECK (LEN(LTRIM(RTRIM(AppVersion))) > 0)
);
GO
CREATE TABLE dbo.ApplicationPackages
(
Id UNIQUEIDENTIFIER NOT NULL
CONSTRAINT PK_ApplicationPackages PRIMARY KEY CLUSTERED
CONSTRAINT DF_ApplicationPackages_Id DEFAULT NEWSEQUENTIALID(),
ApplicationId UNIQUEIDENTIFIER NOT NULL,
PackageId UNIQUEIDENTIFIER NOT NULL,
SelectedVersionId UNIQUEIDENTIFIER NULL,
AddedAt DATETIME2(3) NOT NULL
CONSTRAINT DF_ApplicationPackages_AddedAt DEFAULT SYSUTCDATETIME(),
Notes NVARCHAR(500) NULL,
CONSTRAINT FK_ApplicationPackages_Application
FOREIGN KEY (ApplicationId) REFERENCES dbo.Applications(Id) ON DELETE CASCADE,
CONSTRAINT FK_ApplicationPackages_Package
FOREIGN KEY (PackageId) REFERENCES dbo.Packages(Id) ON DELETE CASCADE
);
GO
CREATE UNIQUE INDEX UX_Users_Username ON dbo.Users(Username);
CREATE UNIQUE INDEX UX_Users_Email ON dbo.Users(Email);
GO
CREATE UNIQUE INDEX UX_EmailConfirmationTokens_TokenHash
ON dbo.EmailConfirmationTokens(TokenHash);
CREATE INDEX IX_EmailConfirmationTokens_UserId
ON dbo.EmailConfirmationTokens(UserId);
GO
CREATE UNIQUE INDEX UX_Packages_PackageCode ON dbo.Packages(PackageCode);
CREATE INDEX IX_Packages_CreatedByUserId ON dbo.Packages(CreatedByUserId);
CREATE INDEX IX_Packages_PackageType ON dbo.Packages(PackageType);
GO
CREATE UNIQUE INDEX UX_PackageVersions_PackageId_Version
ON dbo.PackageVersions(PackageId, Version);
CREATE UNIQUE INDEX UX_PackageVersions_Id_PackageId
ON dbo.PackageVersions(Id, PackageId);
CREATE UNIQUE INDEX UX_PackageVersions_OneLatestPerPackage
ON dbo.PackageVersions(PackageId)
WHERE IsLatest = 1;
CREATE INDEX IX_PackageVersions_PackageId_ReleaseDate
ON dbo.PackageVersions(PackageId, ReleaseDate DESC);
GO
CREATE UNIQUE INDEX UX_Applications_AppCode
ON dbo.Applications(AppCode);
CREATE INDEX IX_Applications_CreatedByUserId ON dbo.Applications(CreatedByUserId);
CREATE INDEX IX_Applications_Status ON dbo.Applications(Status);
GO
CREATE UNIQUE INDEX UX_ApplicationPackages_ApplicationId_PackageId
ON dbo.ApplicationPackages(ApplicationId, PackageId);
CREATE INDEX IX_ApplicationPackages_PackageId
ON dbo.ApplicationPackages(PackageId);
CREATE INDEX IX_ApplicationPackages_SelectedVersionId
ON dbo.ApplicationPackages(SelectedVersionId);
GO
ALTER TABLE dbo.ApplicationPackages
ADD CONSTRAINT FK_ApplicationPackages_SelectedVersionBelongsToPackage
FOREIGN KEY (SelectedVersionId, PackageId)
REFERENCES dbo.PackageVersions(Id, PackageId);
GO
CREATE OR ALTER PROCEDURE dbo.SetLatestPackageVersion
@PackageVersionId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @PackageId UNIQUEIDENTIFIER;
SELECT @PackageId = PackageId
FROM dbo.PackageVersions
WHERE Id = @PackageVersionId;
IF @PackageId IS NULL
BEGIN
THROW 50002, 'Package version does not exist.', 1;
END;
BEGIN TRANSACTION;
UPDATE dbo.PackageVersions
SET IsLatest = 0
WHERE PackageId = @PackageId;
UPDATE dbo.PackageVersions
SET IsLatest = 1,
IsDeprecated = 0
WHERE Id = @PackageVersionId;
COMMIT TRANSACTION;
END;
GO
CREATE OR ALTER PROCEDURE dbo.DeletePackageVersion
@PackageVersionId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @PackageId UNIQUEIDENTIFIER;
DECLARE @WasLatest BIT;
SELECT
@PackageId = PackageId,
@WasLatest = IsLatest
FROM dbo.PackageVersions
WHERE Id = @PackageVersionId;
IF @PackageId IS NULL
BEGIN
THROW 50003, 'Package version does not exist.', 1;
END;
BEGIN TRANSACTION;
DELETE FROM dbo.ApplicationPackages
WHERE SelectedVersionId = @PackageVersionId;
DELETE FROM dbo.PackageVersions
WHERE Id = @PackageVersionId;
IF @WasLatest = 1
BEGIN
DECLARE @NextLatestId UNIQUEIDENTIFIER;
SELECT TOP (1) @NextLatestId = Id
FROM dbo.PackageVersions
WHERE PackageId = @PackageId
AND IsDeprecated = 0
ORDER BY ReleaseDate DESC, UploadedAt DESC;
IF @NextLatestId IS NOT NULL
BEGIN
EXEC dbo.SetLatestPackageVersion @NextLatestId;
END;
END;
COMMIT TRANSACTION;
END;
GO
PRINT N'RobotInstaller schema was created successfully.';
GO

View File

@@ -0,0 +1,131 @@
USE [RobotInstaller];
GO
CREATE OR ALTER VIEW dbo.vw_PackageList
AS
SELECT
p.Id,
p.PackageCode,
p.PackageName,
p.PackageType,
p.Description,
p.IsActive,
p.CreatedAt,
p.UpdatedAt,
p.CreatedByUserId,
u.Username AS CreatedByUsername,
latest.Id AS LatestVersionId,
latest.Version AS LatestVersion,
latest.ReleaseDate AS LatestReleaseDate,
latest.FilePath AS LatestFilePath,
latest.DockerImage AS LatestDockerImage,
version_count.VersionCount
FROM dbo.Packages AS p
INNER JOIN dbo.Users AS u
ON u.Id = p.CreatedByUserId
OUTER APPLY
(
SELECT TOP (1)
pv.Id,
pv.Version,
pv.ReleaseDate,
pv.FilePath,
pv.DockerImage
FROM dbo.PackageVersions AS pv
WHERE pv.PackageId = p.Id
ORDER BY pv.IsLatest DESC, pv.ReleaseDate DESC, pv.UploadedAt DESC
) AS latest
OUTER APPLY
(
SELECT COUNT_BIG(*) AS VersionCount
FROM dbo.PackageVersions AS pv
WHERE pv.PackageId = p.Id
) AS version_count;
GO
CREATE OR ALTER VIEW dbo.vw_PackageVersionList
AS
SELECT
pv.Id,
pv.PackageId,
p.PackageCode,
p.PackageName,
p.PackageType,
pv.Version,
pv.FilePath,
pv.DockerImage,
pv.FileChecksumSha256,
pv.FileSizeBytes,
pv.ChangeLog,
pv.ReleaseDate,
pv.UploadedAt,
pv.IsLatest,
pv.IsDeprecated
FROM dbo.PackageVersions AS pv
INNER JOIN dbo.Packages AS p
ON p.Id = pv.PackageId;
GO
CREATE OR ALTER VIEW dbo.vw_ApplicationList
AS
SELECT
a.Id,
a.AppCode,
a.AppName,
a.AppVersion,
a.Description,
a.Status,
a.Notes,
a.CreatedAt,
a.UpdatedAt,
a.CreatedByUserId,
u.Username AS CreatedByUsername,
COUNT_BIG(ap.Id) AS PackageCount
FROM dbo.Applications AS a
INNER JOIN dbo.Users AS u
ON u.Id = a.CreatedByUserId
LEFT JOIN dbo.ApplicationPackages AS ap
ON ap.ApplicationId = a.Id
GROUP BY
a.Id,
a.AppCode,
a.AppName,
a.AppVersion,
a.Description,
a.Status,
a.Notes,
a.CreatedAt,
a.UpdatedAt,
a.CreatedByUserId,
u.Username;
GO
CREATE OR ALTER VIEW dbo.vw_ApplicationPackageDetails
AS
SELECT
ap.Id,
ap.ApplicationId,
a.AppCode,
a.AppName,
a.AppVersion,
ap.PackageId,
p.PackageCode,
p.PackageName,
p.PackageType,
ap.SelectedVersionId,
pv.Version AS SelectedVersion,
pv.FilePath,
pv.DockerImage,
ap.AddedAt,
ap.Notes
FROM dbo.ApplicationPackages AS ap
INNER JOIN dbo.Applications AS a
ON a.Id = ap.ApplicationId
INNER JOIN dbo.Packages AS p
ON p.Id = ap.PackageId
LEFT JOIN dbo.PackageVersions AS pv
ON pv.Id = ap.SelectedVersionId;
GO
PRINT N'RobotInstaller views were created successfully.';
GO

View File

@@ -0,0 +1,93 @@
# RobotInstaller database
Thiết kế này bám theo `database.md` và sơ đồ database hiện có, sau đó bổ sung vài cột cần cho web server:
- `PackageType`: phân biệt package `.deb``docker`.
- `AppVersion`: version hiện tại của app đóng gói.
- metadata artifact: `DockerImage`, `FileChecksumSha256`, `FileSizeBytes`, `UploadedAt`.
- `Role`, `IsActive` cho tài khoản đăng nhập web.
## Database
Tên database được chọn: `RobotInstaller`
Server:
```powershell
172.20.235.176
```
Không lưu mật khẩu thật vào file cấu hình. Khi chạy local, tạo file `.env` từ `web-server/.env.example` rồi điền mật khẩu thật.
## Cấu trúc chính
| Bảng | Vai trò |
| --- | --- |
| `dbo.Users` | Người dùng web server |
| `dbo.Packages` | Danh mục package |
| `dbo.PackageVersions` | Các version của từng package |
| `dbo.Applications` | App được đóng gói từ nhiều package |
| `dbo.ApplicationPackages` | Liên kết app-package, có thể chọn version cụ thể |
## Ràng buộc quan trọng
- `Users.Username`, `Users.Email` là duy nhất.
- `Packages.PackageCode` là duy nhất.
- `Applications.AppCode` là duy nhất.
- Mỗi package không được trùng `Version`.
- Mỗi app chỉ chứa một dòng cho mỗi package.
- Mỗi package chỉ có tối đa một `IsLatest = 1`.
- `ApplicationPackages.SelectedVersionId` bắt buộc thuộc đúng `PackageId` trên cùng dòng.
## View cho API
| View | Dùng cho màn hình |
| --- | --- |
| `dbo.vw_PackageList` | Danh sách package kèm latest version |
| `dbo.vw_PackageVersionList` | Chi tiết version của package |
| `dbo.vw_ApplicationList` | Danh sách app kèm số package |
| `dbo.vw_ApplicationPackageDetails` | Chi tiết package/version trong app |
## Stored procedure
| Procedure | Mục đích |
| --- | --- |
| `dbo.SetLatestPackageVersion` | Đặt một version là latest và tự clear latest cũ |
| `dbo.DeletePackageVersion` | Xóa version và các liên kết app đang dùng version đó |
## Triển khai bằng sqlcmd
```powershell
$env:SQLCMDPASSWORD = '<mat-khau-sa>'
sqlcmd -S 172.20.235.176 -U sa -b -i .\database\01_create_database.sql
sqlcmd -S 172.20.235.176 -U sa -d RobotInstaller -b -i .\database\02_schema.sql
sqlcmd -S 172.20.235.176 -U sa -d RobotInstaller -b -i .\database\03_views.sql
```
Chạy các lệnh trên từ thư mục `web-server`.
Khi dùng `sqlcmd` để seed/test dữ liệu, thêm `-I` hoặc bật `SET QUOTED_IDENTIFIER ON` vì schema có filtered index cho ràng buộc một latest version trên mỗi package.
## Luồng dữ liệu đề xuất
1. Upload package mới:
- nếu package chưa tồn tại, insert vào `Packages`;
- insert version vào `PackageVersions`;
- gọi `dbo.SetLatestPackageVersion @PackageVersionId`.
2. Update package:
- insert thêm một dòng mới vào `PackageVersions`;
- gọi `dbo.SetLatestPackageVersion @PackageVersionId`.
3. Tạo app:
- insert vào `Applications`;
- insert các package được chọn vào `ApplicationPackages`;
- nếu user chọn version cụ thể, điền `SelectedVersionId`.
4. Xóa package:
- xóa dòng trong `Packages`;
- database tự cascade sang `PackageVersions``ApplicationPackages`.
5. Xóa version:
- gọi `dbo.DeletePackageVersion @PackageVersionId` để xóa cả liên kết app đang dùng version đó.