完善域名服务器多对多关联
新增服务器反向读取托管域名接口,补充多对多关联回归测试与管理页面展示,并更新整改进度清单。
This commit is contained in:
parent
cf2df5ee66
commit
35c9d7760c
@ -44,6 +44,24 @@ func TestProjectBindingAPIManyToManyAndPathConflict(t *testing.T) {
|
||||
t.Fatalf("冲突提示不明确或泄露其他域名:%s", response.Body.String())
|
||||
}
|
||||
}
|
||||
assertServerDomains := func(id string, expected []string) {
|
||||
t.Helper()
|
||||
response := serveUserRequest(f.server, f.cookies["manager"], http.MethodGet, "/api/v1/servers/"+id+"/domains", "")
|
||||
var result struct {
|
||||
Items []domain.Domain `json:"items"`
|
||||
}
|
||||
if response.Code != http.StatusOK || json.Unmarshal(response.Body.Bytes(), &result) != nil || result.Items == nil {
|
||||
t.Fatalf("服务器反向关联读取响应错误:%d,%s", response.Code, response.Body.String())
|
||||
}
|
||||
ids := make([]string, 0, len(result.Items))
|
||||
for _, item := range result.Items {
|
||||
ids = append(ids, item.ID)
|
||||
}
|
||||
slices.Sort(ids)
|
||||
if !slices.Equal(ids, expected) {
|
||||
t.Fatalf("服务器反向关联不正确:%s,%v,期望 %v", id, ids, expected)
|
||||
}
|
||||
}
|
||||
assertBindings := func(id string, expected []string) {
|
||||
t.Helper()
|
||||
response := serveUserRequest(f.server, f.cookies["manager"], http.MethodGet, "/api/v1/domains/"+id+"/servers", "")
|
||||
@ -67,9 +85,13 @@ func TestProjectBindingAPIManyToManyAndPathConflict(t *testing.T) {
|
||||
put("domain-c", `{"server_ids":["server-a","server-c"]}`, http.StatusOK)
|
||||
assertBindings("domain-a", both)
|
||||
assertBindings("domain-c", both)
|
||||
assertServerDomains("server-a", []string{"domain-a", "domain-c"})
|
||||
assertServerDomains("server-c", []string{"domain-a", "domain-c"})
|
||||
put("domain-a", `{"server_ids":["server-c"]}`, http.StatusOK)
|
||||
assertBindings("domain-a", both[1:])
|
||||
assertBindings("domain-c", both)
|
||||
assertServerDomains("server-a", []string{"domain-c"})
|
||||
assertServerDomains("server-c", []string{"domain-a", "domain-c"})
|
||||
put("domain-a", `{"server_ids":[]}`, http.StatusOK)
|
||||
assertBindings("domain-a", []string{})
|
||||
assertBindings("domain-c", both)
|
||||
|
||||
@ -16,6 +16,7 @@ func (s *Server) registerProjectResourceRoutes() {
|
||||
api.GET("/audit-logs", s.listAuditLogs)
|
||||
api.GET("/servers", s.listServers)
|
||||
api.GET("/servers/:id", s.getServer)
|
||||
api.GET("/servers/:id/domains", s.listServerDomains)
|
||||
api.GET("/domains/:id/servers", s.listDomainServers)
|
||||
api.GET("/certificates", s.listCertificates)
|
||||
api.GET("/certificates/:id", s.getCertificate)
|
||||
|
||||
@ -98,6 +98,7 @@ func TestProjectResourceAPIDetailsHideExistenceAndGateFiles(t *testing.T) {
|
||||
"/api/v1/certificates/domain-" + suffix,
|
||||
"/api/v1/domains/domain-" + suffix + "/certificate/versions",
|
||||
"/api/v1/domains/domain-" + suffix + "/servers",
|
||||
"/api/v1/servers/server-" + suffix + "/domains",
|
||||
} {
|
||||
reader.reads = nil
|
||||
response := serveUserRequest(f.server, f.cookies["viewer"], http.MethodGet, path, "")
|
||||
@ -152,6 +153,7 @@ func TestProjectResourceAPIMissingDependencyFailsClosed(t *testing.T) {
|
||||
for _, path := range []string{
|
||||
"/api/v1/domains", "/api/v1/servers", "/api/v1/servers/server-a", "/api/v1/certificates",
|
||||
"/api/v1/certificates/domain-a", "/api/v1/domains/domain-a/certificate/versions", "/api/v1/domains/domain-a/servers",
|
||||
"/api/v1/servers/server-a/domains",
|
||||
} {
|
||||
response := serveUserRequest(f.server, f.cookies["primary-super-admin"], http.MethodGet, path, "")
|
||||
if response.Code != http.StatusServiceUnavailable || len(reader.reads) != 0 {
|
||||
|
||||
@ -1069,6 +1069,18 @@ func (s *Server) listDomainServers(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (s *Server) listServerDomains(c *gin.Context) {
|
||||
if !s.requireProjectResourceReader(c) {
|
||||
return
|
||||
}
|
||||
items, err := s.projectResources.ListVisibleServerDomains(c.Request.Context(), c.GetString(authenticatedActorKey), c.Param("id"))
|
||||
if err != nil {
|
||||
s.writeProjectResourceError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func serverFromRequest(request serverRequest) deployment.Server {
|
||||
return deployment.Server{
|
||||
ID: domain.NewID(), Name: request.Name, Address: request.Address, Port: request.Port,
|
||||
|
||||
@ -65,6 +65,19 @@ func (r projectResourceReaderStub) ListVisibleDomainServers(ctx context.Context,
|
||||
return r.servers.ListServersByDomainID(ctx, id)
|
||||
}
|
||||
|
||||
func (r projectResourceReaderStub) ListVisibleServerDomains(ctx context.Context, _ string, id string) ([]domain.Domain, error) {
|
||||
servers, err := r.servers.ListServers(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, server := range servers {
|
||||
if server.ID == id {
|
||||
return []domain.Domain{}, nil
|
||||
}
|
||||
}
|
||||
return nil, repository.ErrProjectAccessDenied
|
||||
}
|
||||
|
||||
func readyTestConfig(directory string) config.Config {
|
||||
cfg := config.Default(directory)
|
||||
// Linux 默认目录属于正式安装,测试必须显式隔离所有可写路径。
|
||||
|
||||
@ -18,6 +18,7 @@ type ProjectResourceReader interface {
|
||||
ListVisibleServers(context.Context, string) ([]deployment.Server, error)
|
||||
GetVisibleServer(context.Context, string, string) (deployment.Server, error)
|
||||
ListVisibleDomainServers(context.Context, string, string) ([]deployment.Server, error)
|
||||
ListVisibleServerDomains(context.Context, string, string) ([]domain.Domain, error)
|
||||
}
|
||||
|
||||
var _ ProjectResourceReader = (*SQLProjectMemberRepository)(nil)
|
||||
@ -142,6 +143,36 @@ func (r *SQLProjectMemberRepository) ListVisibleDomainServers(ctx context.Contex
|
||||
WHERE `+domainPredicate+` AND `+serverPredicate+` AND domains.id = ? ORDER BY servers.name, servers.id`, args)
|
||||
}
|
||||
|
||||
func (r *SQLProjectMemberRepository) ListVisibleServerDomains(ctx context.Context, actorID, serverID string) ([]domain.Domain, error) {
|
||||
if _, err := r.GetVisibleServer(ctx, actorID, serverID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
domainPredicate, args := resourceReadPredicate("domains.project_id", actorID, domain.PermissionCertificateRead)
|
||||
serverPredicate, serverArgs := resourceReadPredicate("servers.project_id", actorID, domain.PermissionServerRead)
|
||||
args = append(args, serverArgs...)
|
||||
args = append(args, serverID)
|
||||
rows, err := r.db.QueryContext(ctx, Rebind(r.driver, visibleDomainSelect+`
|
||||
JOIN domain_servers visible_binding ON visible_binding.domain_id = domains.id
|
||||
JOIN servers ON servers.id = visible_binding.server_id AND servers.project_id = domains.project_id
|
||||
WHERE `+domainPredicate+` AND `+serverPredicate+` AND servers.id = ? ORDER BY domains.name, domains.id`), args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query visible server domains: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]domain.Domain, 0)
|
||||
for rows.Next() {
|
||||
item, err := scanDomain(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read visible server domains: %w", err)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *SQLProjectMemberRepository) readVisibleServers(ctx context.Context, query string, args []any) ([]deployment.Server, error) {
|
||||
rows, err := r.db.QueryContext(ctx, Rebind(r.driver, query), args...)
|
||||
if err != nil {
|
||||
|
||||
@ -3430,3 +3430,17 @@ Linux Ubuntu 22.04 上的 `Scert-admin` 手动启停、无开机自启动、无
|
||||
- `[!]` PostgreSQL/MySQL 矩阵、浏览器验收、远端 SSH、HTTPS/DNS/ACME 和生产部署验收尚未执行。
|
||||
|
||||
下一步继续开发剩余任务与证书/部署权限链,开发阶段结束后再统一执行三库、浏览器和远端生产流程测试。
|
||||
|
||||
## 本轮开发进度(2026-09-29 19:50)
|
||||
|
||||
按用户要求,总体验收位置继续保持 **P0-03 完成待复审**;本轮完成“域名与服务器多对多部署关联”开发,不将本地测试结果视为生产验收通过。
|
||||
|
||||
- `[x]` 保留并验证域名绑定多台服务器的事务模型,单次替换只影响当前域名,不覆盖其他域名的服务器关联。
|
||||
- `[x]` 新增服务器反向读取关联域名的项目资源接口:`GET /api/v1/servers/:id/domains`。
|
||||
- `[x]` 服务器关联域名读取按实时账户状态、项目成员权限、服务器项目和域名项目同时过滤,未知或越权资源统一拒绝。
|
||||
- `[x]` 管理页面在服务器卡片展示当前托管的全部域名,解绑一个域名时不影响同服务器的其他域名。
|
||||
- `[x]` 新增多对多反向关联回归测试;后端 `go test ./...`、`go vet ./...`、前端 230 项测试和前端生产构建通过。
|
||||
- `[~]` P0-07 其他任务执行、证书部署、续期、审计完整覆盖及跨数据库矩阵仍待继续开发或复审。
|
||||
- `[!]` 浏览器清缓存验收、远端密码 SSH、HTTPS/DNS/ACME、Linux/Windows 服务和生产部署验收仍未执行。
|
||||
|
||||
下一步:继续完成开发阶段剩余权限与部署闭环,之后统一按生产流程编译、部署和测试;验收位置仍以 P0-03 为准。
|
||||
@ -725,3 +725,17 @@ P0-05 从 `[!]` 调整为 `[~]`,并未标记完成;P0-04 剩余门禁和全
|
||||
- **自动化验证:** 后端 HTTP API/配置专项通过,前端 230 项测试通过,前端生产构建通过;这些结果不等同于生产部署验收。
|
||||
- **仍在开发:** 续期任务、证书导入/部署、任务执行/重试及其他资源写入的完整实时授权和拒绝副作用验证。
|
||||
- **尚未执行:** PostgreSQL/MySQL 矩阵、浏览器清缓存验收、远端密码 SSH、HTTPS/DNS/ACME 和生产部署流程。
|
||||
|
||||
## 本轮开发进度(2026-09-29 19:50)
|
||||
|
||||
按用户要求,总体验收位置继续保持 **P0-03 完成待复审**;本轮完成“域名与服务器多对多部署关联”开发,不将本地测试结果视为生产验收通过。
|
||||
|
||||
- `[x]` 保留并验证域名绑定多台服务器的事务模型,单次替换只影响当前域名,不覆盖其他域名的服务器关联。
|
||||
- `[x]` 新增服务器反向读取关联域名的项目资源接口:`GET /api/v1/servers/:id/domains`。
|
||||
- `[x]` 服务器关联域名读取按实时账户状态、项目成员权限、服务器项目和域名项目同时过滤,未知或越权资源统一拒绝。
|
||||
- `[x]` 管理页面在服务器卡片展示当前托管的全部域名,解绑一个域名时不影响同服务器的其他域名。
|
||||
- `[x]` 新增多对多反向关联回归测试;后端 `go test ./...`、`go vet ./...`、前端 230 项测试和前端生产构建通过。
|
||||
- `[~]` P0-07 其他任务执行、证书部署、续期、审计完整覆盖及跨数据库矩阵仍待继续开发或复审。
|
||||
- `[!]` 浏览器清缓存验收、远端密码 SSH、HTTPS/DNS/ACME、Linux/Windows 服务和生产部署验收仍未执行。
|
||||
|
||||
下一步:继续完成开发阶段剩余权限与部署闭环,之后统一按生产流程编译、部署和测试;验收位置仍以 P0-03 为准。
|
||||
@ -1364,3 +1364,17 @@ P0-11 生产验收门禁
|
||||
- `[x]` 后端专项测试、静态检查、前端测试和生产构建通过。
|
||||
- `[~]` P0-07 其他任务/证书/部署权限链继续开发。
|
||||
- `[!]` 三数据库、浏览器、远端服务器和生产部署验收仍未开始。
|
||||
|
||||
## 本轮开发进度(2026-09-29 19:50)
|
||||
|
||||
按用户要求,总体验收位置继续保持 **P0-03 完成待复审**;本轮完成“域名与服务器多对多部署关联”开发,不将本地测试结果视为生产验收通过。
|
||||
|
||||
- `[x]` 保留并验证域名绑定多台服务器的事务模型,单次替换只影响当前域名,不覆盖其他域名的服务器关联。
|
||||
- `[x]` 新增服务器反向读取关联域名的项目资源接口:`GET /api/v1/servers/:id/domains`。
|
||||
- `[x]` 服务器关联域名读取按实时账户状态、项目成员权限、服务器项目和域名项目同时过滤,未知或越权资源统一拒绝。
|
||||
- `[x]` 管理页面在服务器卡片展示当前托管的全部域名,解绑一个域名时不影响同服务器的其他域名。
|
||||
- `[x]` 新增多对多反向关联回归测试;后端 `go test ./...`、`go vet ./...`、前端 230 项测试和前端生产构建通过。
|
||||
- `[~]` P0-07 其他任务执行、证书部署、续期、审计完整覆盖及跨数据库矩阵仍待继续开发或复审。
|
||||
- `[!]` 浏览器清缓存验收、远端密码 SSH、HTTPS/DNS/ACME、Linux/Windows 服务和生产部署验收仍未执行。
|
||||
|
||||
下一步:继续完成开发阶段剩余权限与部署闭环,之后统一按生产流程编译、部署和测试;验收位置仍以 P0-03 为准。
|
||||
@ -315,3 +315,17 @@ P0-06 与 P0-07 均维持 `[~]`,六阶段中的隔离、生产和最终验收
|
||||
- `[!]` PostgreSQL/MySQL 矩阵、浏览器验收、Linux/Windows 生产部署、远端 SSH、HTTPS/DNS/ACME 和 `odacm.cn` 实机验收尚未执行。
|
||||
|
||||
下一步继续开发 DNS 账户及任务相关权限链,开发阶段结束后统一执行生产构建、部署和验收流程。
|
||||
|
||||
## 本轮开发进度(2026-09-29 19:50)
|
||||
|
||||
按用户要求,总体验收位置继续保持 **P0-03 完成待复审**;本轮完成“域名与服务器多对多部署关联”开发,不将本地测试结果视为生产验收通过。
|
||||
|
||||
- `[x]` 保留并验证域名绑定多台服务器的事务模型,单次替换只影响当前域名,不覆盖其他域名的服务器关联。
|
||||
- `[x]` 新增服务器反向读取关联域名的项目资源接口:`GET /api/v1/servers/:id/domains`。
|
||||
- `[x]` 服务器关联域名读取按实时账户状态、项目成员权限、服务器项目和域名项目同时过滤,未知或越权资源统一拒绝。
|
||||
- `[x]` 管理页面在服务器卡片展示当前托管的全部域名,解绑一个域名时不影响同服务器的其他域名。
|
||||
- `[x]` 新增多对多反向关联回归测试;后端 `go test ./...`、`go vet ./...`、前端 230 项测试和前端生产构建通过。
|
||||
- `[~]` P0-07 其他任务执行、证书部署、续期、审计完整覆盖及跨数据库矩阵仍待继续开发或复审。
|
||||
- `[!]` 浏览器清缓存验收、远端密码 SSH、HTTPS/DNS/ACME、Linux/Windows 服务和生产部署验收仍未执行。
|
||||
|
||||
下一步:继续完成开发阶段剩余权限与部署闭环,之后统一按生产流程编译、部署和测试;验收位置仍以 P0-03 为准。
|
||||
@ -465,6 +465,21 @@ code {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ssh-server-domain-bindings {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border: 1px solid #e4ebf4;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.ssh-server-domain-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.ssh-server-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
@ -552,7 +567,8 @@ code {
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.ssh-server-details {
|
||||
|
||||
.ssh-server-details {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@ -565,7 +581,8 @@ code {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ssh-server-details {
|
||||
|
||||
.ssh-server-details {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
|
||||
@ -24,6 +24,14 @@ export function useDashboard() {
|
||||
const settings = ref(null);
|
||||
const dnsProviderCatalog = ref([]);
|
||||
const domainServers = ref({});
|
||||
const serverDomains = computed(() => Object.fromEntries(
|
||||
servers.value.map((server) => [
|
||||
server.id,
|
||||
domains.value.filter((domain) =>
|
||||
(domainServers.value[domain.id] || []).some((item) => item.id === server.id),
|
||||
),
|
||||
]),
|
||||
));
|
||||
const domainPage = ref(1);
|
||||
const domainPageSize = ref(5);
|
||||
const domainPageSizeOptions = [5, 10, 15, 30];
|
||||
@ -1213,6 +1221,7 @@ export function useDashboard() {
|
||||
clearDNSCredentials,
|
||||
testDNS,
|
||||
domainServers,
|
||||
serverDomains,
|
||||
domainPage,
|
||||
domainPageSize,
|
||||
domainPageSizeOptions,
|
||||
|
||||
@ -25,6 +25,7 @@ const {
|
||||
settings,
|
||||
settingsForm,
|
||||
domainServers,
|
||||
serverDomains,
|
||||
domainPage,
|
||||
domainPageSize,
|
||||
domainPageSizeOptions,
|
||||
@ -459,6 +460,15 @@ const { activeSection, activeLabel, sections, selectSection } = useDashboardNavi
|
||||
<span>主机指纹:{{ item.host_key_fingerprint || "未填写" }}</span>
|
||||
<span>认证:{{ serverAuthLabel(item) }}</span>
|
||||
</div>
|
||||
<div class="ssh-server-domain-bindings">
|
||||
<span class="detail-label">托管域名</span>
|
||||
<div v-if="(serverDomains[item.id] || []).length > 0" class="ssh-server-domain-list">
|
||||
<span v-for="domain in serverDomains[item.id]" :key="domain.id" class="tag neutral">
|
||||
{{ domain.name }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-else class="muted">未关联托管域名</span>
|
||||
</div>
|
||||
<div class="ssh-server-details">
|
||||
<div class="ssh-server-detail">
|
||||
<span class="detail-label">托管私钥</span>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user