@atomservice/config 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/server/src/db/migrations.ts +110 -82
- package/server/src/http/router.ts +2 -0
- package/server/src/modules/setup.ts +35 -0
- package/server/src/shared/bootstrap.ts +1 -3
- package/src/config.compose.ts +0 -6
- package/src/config.options.ts +0 -21
- package/src/config.service.ts +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atomservice/config",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "配置中心原子服务:基于 Bun 的高性能配置管理服务",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "openorson",
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
".": "./src/index.ts"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@atomservice/gateway": "0.1.
|
|
32
|
+
"@atomservice/gateway": "0.1.13"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@atomservice/core": "0.1.
|
|
35
|
+
"@atomservice/core": "0.1.13"
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -2,105 +2,133 @@ import type { Migration } from "./types.ts"
|
|
|
2
2
|
|
|
3
3
|
export const migrations: Migration[] = [
|
|
4
4
|
{
|
|
5
|
-
id: "
|
|
5
|
+
id: "001_users",
|
|
6
6
|
up: `
|
|
7
7
|
CREATE TABLE "User" (
|
|
8
|
-
id
|
|
9
|
-
email
|
|
10
|
-
name
|
|
11
|
-
passwordHash
|
|
12
|
-
isSuperAdmin
|
|
13
|
-
createdAt
|
|
14
|
-
updatedAt
|
|
8
|
+
"id" TEXT PRIMARY KEY,
|
|
9
|
+
"email" TEXT NOT NULL UNIQUE,
|
|
10
|
+
"name" TEXT NOT NULL,
|
|
11
|
+
"passwordHash" TEXT NOT NULL,
|
|
12
|
+
"isSuperAdmin" INTEGER NOT NULL DEFAULT 0,
|
|
13
|
+
"createdAt" INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
14
|
+
"updatedAt" INTEGER NOT NULL DEFAULT (unixepoch())
|
|
15
15
|
);
|
|
16
|
-
|
|
16
|
+
`,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "002_tokens",
|
|
20
|
+
up: `
|
|
17
21
|
CREATE TABLE "Token" (
|
|
18
|
-
id
|
|
19
|
-
userId
|
|
20
|
-
name
|
|
21
|
-
tokenHash
|
|
22
|
-
tokenPrefix
|
|
23
|
-
expiresAt
|
|
24
|
-
lastUsedAt
|
|
25
|
-
revoked
|
|
26
|
-
createdAt
|
|
22
|
+
"id" TEXT PRIMARY KEY,
|
|
23
|
+
"userId" TEXT NOT NULL REFERENCES "User"("id") ON DELETE CASCADE,
|
|
24
|
+
"name" TEXT,
|
|
25
|
+
"tokenHash" TEXT NOT NULL UNIQUE,
|
|
26
|
+
"tokenPrefix" TEXT NOT NULL,
|
|
27
|
+
"expiresAt" INTEGER,
|
|
28
|
+
"lastUsedAt" INTEGER,
|
|
29
|
+
"revoked" INTEGER NOT NULL DEFAULT 0,
|
|
30
|
+
"createdAt" INTEGER NOT NULL DEFAULT (unixepoch())
|
|
27
31
|
);
|
|
28
|
-
CREATE INDEX
|
|
29
|
-
|
|
32
|
+
CREATE INDEX token_user_idx ON "Token"("userId");
|
|
33
|
+
`,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "003_projects",
|
|
37
|
+
up: `
|
|
30
38
|
CREATE TABLE "Project" (
|
|
31
|
-
id TEXT PRIMARY KEY,
|
|
32
|
-
slug TEXT NOT NULL UNIQUE,
|
|
33
|
-
name TEXT NOT NULL,
|
|
34
|
-
description TEXT,
|
|
35
|
-
createdBy TEXT NOT NULL REFERENCES "User"(id),
|
|
36
|
-
createdAt INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
37
|
-
updatedAt INTEGER NOT NULL DEFAULT (unixepoch())
|
|
39
|
+
"id" TEXT PRIMARY KEY,
|
|
40
|
+
"slug" TEXT NOT NULL UNIQUE,
|
|
41
|
+
"name" TEXT NOT NULL,
|
|
42
|
+
"description" TEXT,
|
|
43
|
+
"createdBy" TEXT NOT NULL REFERENCES "User"("id"),
|
|
44
|
+
"createdAt" INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
45
|
+
"updatedAt" INTEGER NOT NULL DEFAULT (unixepoch())
|
|
38
46
|
);
|
|
39
|
-
|
|
47
|
+
`,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "004_project_members",
|
|
51
|
+
up: `
|
|
40
52
|
CREATE TABLE "ProjectMember" (
|
|
41
|
-
projectId TEXT NOT NULL REFERENCES "Project"(id) ON DELETE CASCADE,
|
|
42
|
-
userId TEXT NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
|
|
43
|
-
role TEXT NOT NULL,
|
|
44
|
-
joinedAt INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
45
|
-
PRIMARY KEY (projectId, userId)
|
|
53
|
+
"projectId" TEXT NOT NULL REFERENCES "Project"("id") ON DELETE CASCADE,
|
|
54
|
+
"userId" TEXT NOT NULL REFERENCES "User"("id") ON DELETE CASCADE,
|
|
55
|
+
"role" TEXT NOT NULL,
|
|
56
|
+
"joinedAt" INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
57
|
+
PRIMARY KEY ("projectId", "userId")
|
|
46
58
|
);
|
|
47
|
-
|
|
59
|
+
`,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "005_environments",
|
|
63
|
+
up: `
|
|
48
64
|
CREATE TABLE "Environment" (
|
|
49
|
-
id TEXT PRIMARY KEY,
|
|
50
|
-
projectId TEXT NOT NULL REFERENCES "Project"(id) ON DELETE CASCADE,
|
|
51
|
-
slug TEXT NOT NULL,
|
|
52
|
-
name TEXT NOT NULL,
|
|
53
|
-
description TEXT,
|
|
54
|
-
sortOrder INTEGER NOT NULL DEFAULT 0,
|
|
55
|
-
sensitive INTEGER NOT NULL DEFAULT 0,
|
|
56
|
-
createdBy TEXT NOT NULL REFERENCES "User"(id),
|
|
57
|
-
createdAt INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
58
|
-
updatedAt INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
59
|
-
UNIQUE (projectId, slug)
|
|
65
|
+
"id" TEXT PRIMARY KEY,
|
|
66
|
+
"projectId" TEXT NOT NULL REFERENCES "Project"("id") ON DELETE CASCADE,
|
|
67
|
+
"slug" TEXT NOT NULL,
|
|
68
|
+
"name" TEXT NOT NULL,
|
|
69
|
+
"description" TEXT,
|
|
70
|
+
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
|
71
|
+
"sensitive" INTEGER NOT NULL DEFAULT 0,
|
|
72
|
+
"createdBy" TEXT NOT NULL REFERENCES "User"("id"),
|
|
73
|
+
"createdAt" INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
74
|
+
"updatedAt" INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
75
|
+
UNIQUE ("projectId", "slug")
|
|
60
76
|
);
|
|
61
|
-
|
|
77
|
+
`,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "006_apps",
|
|
81
|
+
up: `
|
|
62
82
|
CREATE TABLE "App" (
|
|
63
|
-
id TEXT PRIMARY KEY,
|
|
64
|
-
projectId TEXT NOT NULL REFERENCES "Project"(id) ON DELETE CASCADE,
|
|
65
|
-
slug TEXT NOT NULL,
|
|
66
|
-
name TEXT NOT NULL,
|
|
67
|
-
description TEXT,
|
|
68
|
-
schema TEXT NOT NULL,
|
|
69
|
-
createdBy TEXT NOT NULL REFERENCES "User"(id),
|
|
70
|
-
createdAt INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
71
|
-
updatedAt INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
72
|
-
UNIQUE (projectId, slug)
|
|
83
|
+
"id" TEXT PRIMARY KEY,
|
|
84
|
+
"projectId" TEXT NOT NULL REFERENCES "Project"("id") ON DELETE CASCADE,
|
|
85
|
+
"slug" TEXT NOT NULL,
|
|
86
|
+
"name" TEXT NOT NULL,
|
|
87
|
+
"description" TEXT,
|
|
88
|
+
"schema" TEXT NOT NULL,
|
|
89
|
+
"createdBy" TEXT NOT NULL REFERENCES "User"("id"),
|
|
90
|
+
"createdAt" INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
91
|
+
"updatedAt" INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
92
|
+
UNIQUE ("projectId", "slug")
|
|
73
93
|
);
|
|
74
|
-
|
|
94
|
+
`,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "007_releases",
|
|
98
|
+
up: `
|
|
75
99
|
CREATE TABLE "Release" (
|
|
76
|
-
id
|
|
77
|
-
appId
|
|
78
|
-
environmentId
|
|
79
|
-
semver
|
|
80
|
-
changeKind
|
|
81
|
-
status
|
|
82
|
-
notes
|
|
83
|
-
snapshot
|
|
84
|
-
schemaSnapshot
|
|
85
|
-
createdBy
|
|
86
|
-
createdAt
|
|
87
|
-
UNIQUE (appId, environmentId, semver)
|
|
100
|
+
"id" TEXT PRIMARY KEY,
|
|
101
|
+
"appId" TEXT NOT NULL REFERENCES "App"("id") ON DELETE CASCADE,
|
|
102
|
+
"environmentId" TEXT NOT NULL REFERENCES "Environment"("id") ON DELETE CASCADE,
|
|
103
|
+
"semver" TEXT NOT NULL,
|
|
104
|
+
"changeKind" TEXT NOT NULL,
|
|
105
|
+
"status" TEXT NOT NULL DEFAULT 'unpublished',
|
|
106
|
+
"notes" TEXT,
|
|
107
|
+
"snapshot" TEXT NOT NULL,
|
|
108
|
+
"schemaSnapshot" TEXT NOT NULL,
|
|
109
|
+
"createdBy" TEXT NOT NULL REFERENCES "User"("id"),
|
|
110
|
+
"createdAt" INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
111
|
+
UNIQUE ("appId", "environmentId", "semver")
|
|
88
112
|
);
|
|
89
|
-
CREATE INDEX
|
|
90
|
-
|
|
113
|
+
CREATE INDEX release_lookup_idx ON "Release"("appId", "environmentId", "createdAt");
|
|
114
|
+
`,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "008_app_tokens",
|
|
118
|
+
up: `
|
|
91
119
|
CREATE TABLE "AppToken" (
|
|
92
|
-
id TEXT PRIMARY KEY,
|
|
93
|
-
appId TEXT NOT NULL REFERENCES "App"(id) ON DELETE CASCADE,
|
|
94
|
-
environmentId TEXT NOT NULL REFERENCES "Environment"(id) ON DELETE CASCADE,
|
|
95
|
-
name TEXT,
|
|
96
|
-
tokenHash TEXT NOT NULL UNIQUE,
|
|
97
|
-
tokenPrefix TEXT NOT NULL,
|
|
98
|
-
revoked INTEGER NOT NULL DEFAULT 0,
|
|
99
|
-
lastUsedAt INTEGER,
|
|
100
|
-
createdBy TEXT NOT NULL REFERENCES "User"(id),
|
|
101
|
-
createdAt INTEGER NOT NULL DEFAULT (unixepoch())
|
|
120
|
+
"id" TEXT PRIMARY KEY,
|
|
121
|
+
"appId" TEXT NOT NULL REFERENCES "App"("id") ON DELETE CASCADE,
|
|
122
|
+
"environmentId" TEXT NOT NULL REFERENCES "Environment"("id") ON DELETE CASCADE,
|
|
123
|
+
"name" TEXT,
|
|
124
|
+
"tokenHash" TEXT NOT NULL UNIQUE,
|
|
125
|
+
"tokenPrefix" TEXT NOT NULL,
|
|
126
|
+
"revoked" INTEGER NOT NULL DEFAULT 0,
|
|
127
|
+
"lastUsedAt" INTEGER,
|
|
128
|
+
"createdBy" TEXT NOT NULL REFERENCES "User"("id"),
|
|
129
|
+
"createdAt" INTEGER NOT NULL DEFAULT (unixepoch())
|
|
102
130
|
);
|
|
103
|
-
CREATE INDEX
|
|
131
|
+
CREATE INDEX app_token_lookup_idx ON "AppToken"("appId", "environmentId");
|
|
104
132
|
`,
|
|
105
133
|
},
|
|
106
134
|
]
|
|
@@ -5,6 +5,7 @@ import { inspectRoutes } from "../modules/inspect.ts"
|
|
|
5
5
|
import { memberRoutes } from "../modules/member.ts"
|
|
6
6
|
import { projectRoutes } from "../modules/project.ts"
|
|
7
7
|
import { releaseRoutes } from "../modules/release.ts"
|
|
8
|
+
import { setupRoutes } from "../modules/setup.ts"
|
|
8
9
|
import { tokenRoutes } from "../modules/token.ts"
|
|
9
10
|
import { userRoutes } from "../modules/user.ts"
|
|
10
11
|
import { ok, route } from "./response.ts"
|
|
@@ -16,6 +17,7 @@ function health(): Response {
|
|
|
16
17
|
|
|
17
18
|
export const routes = {
|
|
18
19
|
"/v1/health": { GET: route(health) },
|
|
20
|
+
...setupRoutes,
|
|
19
21
|
...userRoutes,
|
|
20
22
|
...projectRoutes,
|
|
21
23
|
...memberRoutes,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getDb } from "../db/client.ts"
|
|
2
|
+
import { HttpError, ok, readBody, requireString, route } from "../http/response.ts"
|
|
3
|
+
import { EMAIL_RE } from "../shared/consts.ts"
|
|
4
|
+
import { hashPassword } from "../shared/password.ts"
|
|
5
|
+
import { publicUser } from "../shared/serialize.ts"
|
|
6
|
+
import { newId } from "../shared/utils.ts"
|
|
7
|
+
|
|
8
|
+
async function setup(req: Request): Promise<Response> {
|
|
9
|
+
const db = getDb()
|
|
10
|
+
const hasSuperAdmin = db.query(`SELECT 1 FROM "User" WHERE "isSuperAdmin" = 1 LIMIT 1`).get()
|
|
11
|
+
if (hasSuperAdmin) throw new HttpError(409, "already_setup", "已完成初始化,超管账号已存在")
|
|
12
|
+
|
|
13
|
+
const body = await readBody(req)
|
|
14
|
+
const email = requireString(body, "email", "邮箱")
|
|
15
|
+
const name = requireString(body, "name", "用户名")
|
|
16
|
+
const password = requireString(body, "password", "密码")
|
|
17
|
+
|
|
18
|
+
if (!EMAIL_RE.test(email)) throw new HttpError(400, "invalid_field", "邮箱格式不正确")
|
|
19
|
+
if (password.length < 8) throw new HttpError(400, "invalid_field", "密码至少 8 位")
|
|
20
|
+
|
|
21
|
+
const id = newId()
|
|
22
|
+
db.query(`INSERT INTO "User" ("id", "email", "name", "passwordHash", "isSuperAdmin") VALUES (?, ?, ?, ?, 1)`).run(
|
|
23
|
+
id,
|
|
24
|
+
email,
|
|
25
|
+
name,
|
|
26
|
+
await hashPassword(password),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const created = db.query(`SELECT * FROM "User" WHERE "id" = ?`).get(id)
|
|
30
|
+
return ok({ user: publicUser(created as Parameters<typeof publicUser>[0]) }, 201)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const setupRoutes = {
|
|
34
|
+
"/v1/setup": { POST: route(setup) },
|
|
35
|
+
}
|
|
@@ -10,9 +10,7 @@ export async function bootstrapAdmin(): Promise<void> {
|
|
|
10
10
|
if (hasSuperAdmin) return
|
|
11
11
|
|
|
12
12
|
const { adminEmail: email, adminPassword: password, adminName: name } = getEnv()
|
|
13
|
-
if (!email || !password)
|
|
14
|
-
throw new Error("数据库中尚无超管账号,请通过环境变量 CONFIG_ADMIN_EMAIL 和 CONFIG_ADMIN_PASSWORD 配置初始超管")
|
|
15
|
-
}
|
|
13
|
+
if (!email || !password) return
|
|
16
14
|
|
|
17
15
|
const existing = db.query(`SELECT * FROM "User" WHERE email = ?`).get(email) as UserRow | null
|
|
18
16
|
if (existing) {
|
package/src/config.compose.ts
CHANGED
|
@@ -4,9 +4,6 @@ import { CONFIG_DEFAULT_PORT } from "./config.consts.ts"
|
|
|
4
4
|
export interface ConfigComposeOptions {
|
|
5
5
|
image: string
|
|
6
6
|
dataDir: string
|
|
7
|
-
adminName?: string
|
|
8
|
-
adminEmail?: string
|
|
9
|
-
adminPassword?: string
|
|
10
7
|
environment?: Record<string, string>
|
|
11
8
|
hostPort?: number
|
|
12
9
|
}
|
|
@@ -20,9 +17,6 @@ export function configCompose(id: string, opts: ConfigComposeOptions): string {
|
|
|
20
17
|
if (v === undefined || v === "") return
|
|
21
18
|
lines.push(` - ${k}=${v}`)
|
|
22
19
|
}
|
|
23
|
-
push("CONFIG_ADMIN_NAME", opts.adminName)
|
|
24
|
-
push("CONFIG_ADMIN_EMAIL", opts.adminEmail)
|
|
25
|
-
push("CONFIG_ADMIN_PASSWORD", opts.adminPassword)
|
|
26
20
|
if (opts.environment) {
|
|
27
21
|
for (const [k, v] of Object.entries(opts.environment)) push(k, v)
|
|
28
22
|
}
|
package/src/config.options.ts
CHANGED
|
@@ -21,27 +21,6 @@ export interface ConfigOptions {
|
|
|
21
21
|
* - 需要和 `gateway` 原子一起使用
|
|
22
22
|
*/
|
|
23
23
|
domain?: string
|
|
24
|
-
/**
|
|
25
|
-
* 初始超级管理员用户名
|
|
26
|
-
*
|
|
27
|
-
* - 与 `adminEmail` 和 `adminPassword` 一起填写则容器首次启动时自动创建超管账号
|
|
28
|
-
*
|
|
29
|
-
* @default "admin"
|
|
30
|
-
*/
|
|
31
|
-
adminName?: string
|
|
32
|
-
/**
|
|
33
|
-
* 初始超级管理员邮箱
|
|
34
|
-
*
|
|
35
|
-
* - 与 `adminPassword` 一起填写则容器首次启动时自动创建超管账号
|
|
36
|
-
* - 不填则需首次访问后通过 API 手动创建
|
|
37
|
-
*/
|
|
38
|
-
adminEmail?: string
|
|
39
|
-
/**
|
|
40
|
-
* 初始超级管理员密码
|
|
41
|
-
*
|
|
42
|
-
* - 见 `adminEmail`
|
|
43
|
-
*/
|
|
44
|
-
adminPassword?: string
|
|
45
24
|
/**
|
|
46
25
|
* 自定义环境变量
|
|
47
26
|
*
|
package/src/config.service.ts
CHANGED
|
@@ -61,9 +61,6 @@ export function config(opts: ConfigOptions = {}): CallableService<ConfigInstance
|
|
|
61
61
|
configCompose(containerName, {
|
|
62
62
|
image: imageTag,
|
|
63
63
|
dataDir,
|
|
64
|
-
adminName: opts.adminName,
|
|
65
|
-
adminEmail: opts.adminEmail,
|
|
66
|
-
adminPassword: opts.adminPassword,
|
|
67
64
|
environment: opts.environment,
|
|
68
65
|
hostPort: opts.hostPort,
|
|
69
66
|
}),
|