@atomservice/config 0.1.10 → 0.1.12

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atomservice/config",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
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.10"
32
+ "@atomservice/gateway": "0.1.12"
33
33
  },
34
34
  "peerDependencies": {
35
- "@atomservice/core": "0.1.10"
35
+ "@atomservice/core": "0.1.12"
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: "001_init",
5
+ id: "001_users",
6
6
  up: `
7
7
  CREATE TABLE "User" (
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())
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 TEXT PRIMARY KEY,
19
- userId TEXT NOT NULL REFERENCES "User"(id) ON DELETE CASCADE,
20
- name TEXT,
21
- tokenHash TEXT NOT NULL UNIQUE,
22
- tokenPrefix TEXT NOT NULL,
23
- expiresAt INTEGER,
24
- lastUsedAt INTEGER,
25
- revoked INTEGER NOT NULL DEFAULT 0,
26
- createdAt INTEGER NOT NULL DEFAULT (unixepoch())
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 tokenUserIdx ON "Token"(userId);
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 TEXT PRIMARY KEY,
77
- appId TEXT NOT NULL REFERENCES "App"(id) ON DELETE CASCADE,
78
- environmentId TEXT NOT NULL REFERENCES "Environment"(id) ON DELETE CASCADE,
79
- semver TEXT NOT NULL,
80
- changeKind TEXT NOT NULL,
81
- status TEXT NOT NULL DEFAULT 'unpublished',
82
- notes TEXT,
83
- snapshot TEXT NOT NULL,
84
- schemaSnapshot TEXT NOT NULL,
85
- createdBy TEXT NOT NULL REFERENCES "User"(id),
86
- createdAt INTEGER NOT NULL DEFAULT (unixepoch()),
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 releaseLookupIdx ON "Release"(appId, environmentId, createdAt);
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 appTokenLookupIdx ON "AppToken"(appId, environmentId);
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) {
@@ -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
  }
@@ -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
  *
@@ -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
  }),