@upend/cli 0.1.0 → 0.1.1
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/README.md +4 -4
- package/bin/cli.ts +4 -3
- package/package.json +1 -1
- package/src/commands/dev.ts +4 -2
- package/src/commands/env.ts +35 -0
- package/src/commands/init.ts +109 -19
- package/src/commands/migrate.ts +11 -9
- package/src/lib/bootstrap.sql +43 -0
- package/src/lib/db.ts +1 -0
- package/src/services/gateway/auth-routes.ts +5 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Bun + Hono + Neon Postgres + Caddy. Custom JWT auth. Claude editing sessions wit
|
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
# create a new project
|
|
19
|
-
bunx @upend/cli init my-
|
|
19
|
+
bunx @upend/cli init my-project
|
|
20
20
|
|
|
21
21
|
# follow the prompts — if neonctl is installed, it will:
|
|
22
22
|
# 1. create a Neon database
|
|
@@ -25,7 +25,7 @@ bunx @upend/cli init my-app
|
|
|
25
25
|
# 4. generate RSA signing keys
|
|
26
26
|
# 5. encrypt your .env with dotenvx
|
|
27
27
|
|
|
28
|
-
cd my-
|
|
28
|
+
cd my-project
|
|
29
29
|
|
|
30
30
|
# add your Anthropic API key
|
|
31
31
|
# (edit .env, then re-encrypt)
|
|
@@ -44,7 +44,7 @@ Open http://localhost:4000 — you'll see the dashboard.
|
|
|
44
44
|
## What you get
|
|
45
45
|
|
|
46
46
|
```
|
|
47
|
-
my-
|
|
47
|
+
my-project/
|
|
48
48
|
├── apps/ → hot-deployed frontends (drop files in, they're live)
|
|
49
49
|
├── migrations/
|
|
50
50
|
│ └── 001_init.sql → starter migration
|
|
@@ -211,7 +211,7 @@ bunx upend setup:jwks
|
|
|
211
211
|
import { defineConfig } from "@upend/cli";
|
|
212
212
|
|
|
213
213
|
export default defineConfig({
|
|
214
|
-
name: "my-
|
|
214
|
+
name: "my-project",
|
|
215
215
|
database: process.env.DATABASE_URL,
|
|
216
216
|
dataApi: process.env.NEON_DATA_API,
|
|
217
217
|
deploy: {
|
package/bin/cli.ts
CHANGED
|
@@ -9,6 +9,7 @@ const commands: Record<string, () => Promise<void>> = {
|
|
|
9
9
|
deploy: () => import("../src/commands/deploy").then((m) => m.default(args.slice(1))),
|
|
10
10
|
migrate: () => import("../src/commands/migrate").then((m) => m.default(args.slice(1))),
|
|
11
11
|
infra: () => import("../src/commands/infra").then((m) => m.default(args.slice(1))),
|
|
12
|
+
env: () => import("../src/commands/env").then((m) => m.default(args.slice(1))),
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
if (!command || command === "--help" || command === "-h") {
|
|
@@ -20,8 +21,8 @@ if (!command || command === "--help" || command === "-h") {
|
|
|
20
21
|
upend dev start local dev (services + caddy)
|
|
21
22
|
upend deploy deploy to remote instance
|
|
22
23
|
upend migrate run database migrations
|
|
24
|
+
upend env:set <K> <V> set an env var (decrypts, sets, re-encrypts)
|
|
23
25
|
upend infra:aws provision AWS infrastructure
|
|
24
|
-
upend infra:gcp provision GCP infrastructure
|
|
25
26
|
|
|
26
27
|
options:
|
|
27
28
|
--help, -h show this help
|
|
@@ -36,8 +37,8 @@ if (command === "--version" || command === "-v") {
|
|
|
36
37
|
process.exit(0);
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
// handle infra:
|
|
40
|
-
const cmd = command.startsWith("infra:") ? "infra" : command;
|
|
40
|
+
// handle colon syntax: infra:aws, env:set
|
|
41
|
+
const cmd = command.startsWith("infra:") ? "infra" : command.startsWith("env:") ? "env" : command;
|
|
41
42
|
|
|
42
43
|
if (!commands[cmd]) {
|
|
43
44
|
console.error(`unknown command: ${command}`);
|
package/package.json
CHANGED
package/src/commands/dev.ts
CHANGED
|
@@ -23,7 +23,8 @@ export default async function dev(args: string[]) {
|
|
|
23
23
|
|
|
24
24
|
// start API service
|
|
25
25
|
log.info(`starting api → :${apiPort}`);
|
|
26
|
-
Bun.spawn(["bun", "--watch", `${cliRoot}/src/services/gateway/index.ts`], {
|
|
26
|
+
Bun.spawn(["bunx", "@dotenvx/dotenvx", "run", "--", "bun", "--watch", `${cliRoot}/src/services/gateway/index.ts`], {
|
|
27
|
+
cwd: projectDir,
|
|
27
28
|
env: { ...process.env, API_PORT: apiPort, UPEND_PROJECT: projectDir },
|
|
28
29
|
stdout: "inherit",
|
|
29
30
|
stderr: "inherit",
|
|
@@ -31,7 +32,8 @@ export default async function dev(args: string[]) {
|
|
|
31
32
|
|
|
32
33
|
// start Claude service
|
|
33
34
|
log.info(`starting claude → :${claudePort}`);
|
|
34
|
-
Bun.spawn(["bun", "--watch", `${cliRoot}/src/services/claude/index.ts`], {
|
|
35
|
+
Bun.spawn(["bunx", "@dotenvx/dotenvx", "run", "--", "bun", "--watch", `${cliRoot}/src/services/claude/index.ts`], {
|
|
36
|
+
cwd: projectDir,
|
|
35
37
|
env: { ...process.env, CLAUDE_PORT: claudePort, UPEND_PROJECT: projectDir },
|
|
36
38
|
stdout: "inherit",
|
|
37
39
|
stderr: "inherit",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { log } from "../lib/log";
|
|
2
|
+
import { exec } from "../lib/exec";
|
|
3
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
4
|
+
|
|
5
|
+
export default async function env(args: string[]) {
|
|
6
|
+
const [key, value] = args;
|
|
7
|
+
|
|
8
|
+
if (!key || !value) {
|
|
9
|
+
log.error("usage: upend env:set <KEY> <VALUE>");
|
|
10
|
+
log.dim(" e.g. upend env:set ANTHROPIC_API_KEY sk-ant-...");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// decrypt
|
|
15
|
+
log.info("decrypting .env...");
|
|
16
|
+
await exec(["bunx", "@dotenvx/dotenvx", "decrypt"], { silent: true });
|
|
17
|
+
|
|
18
|
+
// read, update, write
|
|
19
|
+
const envFile = readFileSync(".env", "utf-8");
|
|
20
|
+
const regex = new RegExp(`^${key}=.*$`, "m");
|
|
21
|
+
|
|
22
|
+
let updated: string;
|
|
23
|
+
if (regex.test(envFile)) {
|
|
24
|
+
updated = envFile.replace(regex, `${key}="${value}"`);
|
|
25
|
+
} else {
|
|
26
|
+
updated = envFile.trimEnd() + `\n${key}="${value}"\n`;
|
|
27
|
+
}
|
|
28
|
+
writeFileSync(".env", updated);
|
|
29
|
+
log.success(`${key} set`);
|
|
30
|
+
|
|
31
|
+
// re-encrypt
|
|
32
|
+
log.info("encrypting .env...");
|
|
33
|
+
await exec(["bunx", "@dotenvx/dotenvx", "encrypt"], { silent: true });
|
|
34
|
+
log.success(".env encrypted");
|
|
35
|
+
}
|
package/src/commands/init.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { log } from "../lib/log";
|
|
2
2
|
import { exec, execOrDie, hasCommand } from "../lib/exec";
|
|
3
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "fs";
|
|
4
4
|
import { join, resolve } from "path";
|
|
5
5
|
|
|
6
6
|
export default async function init(args: string[]) {
|
|
7
7
|
const name = args[0];
|
|
8
8
|
if (!name) {
|
|
9
|
-
log.error("usage: upend init <name>");
|
|
9
|
+
log.error("usage: upend init <name> [--admin-email <email> --admin-password <pass>]");
|
|
10
10
|
log.dim(" e.g. upend init beta → deploys to beta.upend.site");
|
|
11
11
|
process.exit(1);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// parse optional flags
|
|
15
|
+
const adminEmail = getFlag(args, "--admin-email");
|
|
16
|
+
const adminPassword = getFlag(args, "--admin-password");
|
|
17
|
+
|
|
14
18
|
const projectDir = resolve(name);
|
|
15
19
|
const domain = `${name}.upend.site`;
|
|
16
20
|
|
|
@@ -46,7 +50,14 @@ export default async function init(args: string[]) {
|
|
|
46
50
|
let neonDataApi = "";
|
|
47
51
|
let neonProjectId = "";
|
|
48
52
|
|
|
49
|
-
if (await hasCommand("neonctl")) {
|
|
53
|
+
if (!(await hasCommand("neonctl"))) {
|
|
54
|
+
log.error("neonctl is required — install with: npm i -g neonctl");
|
|
55
|
+
log.dim("upend currently requires Neon Postgres. More database support coming soon.");
|
|
56
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
{
|
|
50
61
|
// check auth
|
|
51
62
|
const { exitCode } = await exec(["neonctl", "me"], { silent: true });
|
|
52
63
|
if (exitCode !== 0) {
|
|
@@ -125,21 +136,16 @@ export default async function init(args: string[]) {
|
|
|
125
136
|
log.warn("couldn't read neon API token — data API needs manual setup");
|
|
126
137
|
}
|
|
127
138
|
}
|
|
128
|
-
} else {
|
|
129
|
-
log.warn("neonctl not found — install with: npm i -g neonctl");
|
|
130
|
-
log.dim("then re-run: upend init " + name);
|
|
131
|
-
log.dim("or set DATABASE_URL manually in .env");
|
|
132
139
|
}
|
|
133
140
|
|
|
134
141
|
// ── 3. scaffold project files ──
|
|
135
142
|
|
|
136
143
|
log.info("scaffolding project...");
|
|
137
144
|
|
|
138
|
-
// resolve @upend/cli
|
|
145
|
+
// resolve @upend/cli version from our own package.json
|
|
139
146
|
const cliPkgPath = new URL("../../package.json", import.meta.url).pathname;
|
|
140
147
|
const cliPkg = JSON.parse(readFileSync(cliPkgPath, "utf-8"));
|
|
141
|
-
const
|
|
142
|
-
const cliDep = cliPkg.version === "0.1.0" ? `file:${cliRoot}` : `^${cliPkg.version}`;
|
|
148
|
+
const cliDep = `^${cliPkg.version}`;
|
|
143
149
|
|
|
144
150
|
writeFile(projectDir, "upend.config.ts", `import { defineConfig } from "@upend/cli";
|
|
145
151
|
|
|
@@ -175,6 +181,7 @@ ANTHROPIC_API_KEY=
|
|
|
175
181
|
DEPLOY_HOST=
|
|
176
182
|
API_PORT=3001
|
|
177
183
|
CLAUDE_PORT=3002
|
|
184
|
+
SIGNUP_ENABLED=false
|
|
178
185
|
`);
|
|
179
186
|
|
|
180
187
|
writeFile(projectDir, ".env.example", `DATABASE_URL=postgresql://user:pass@host/db?sslmode=require
|
|
@@ -195,11 +202,12 @@ sessions/
|
|
|
195
202
|
|
|
196
203
|
// migrations
|
|
197
204
|
mkdirSync(join(projectDir, "migrations"), { recursive: true });
|
|
198
|
-
writeFile(projectDir, "migrations/
|
|
199
|
-
CREATE TABLE IF NOT EXISTS
|
|
200
|
-
id
|
|
201
|
-
|
|
202
|
-
|
|
205
|
+
writeFile(projectDir, "migrations/001_users.sql", `-- users table (exposed via Data API)
|
|
206
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
207
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
208
|
+
email TEXT UNIQUE NOT NULL,
|
|
209
|
+
password_hash TEXT NOT NULL,
|
|
210
|
+
role TEXT DEFAULT 'user',
|
|
203
211
|
created_at TIMESTAMPTZ DEFAULT now(),
|
|
204
212
|
updated_at TIMESTAMPTZ DEFAULT now()
|
|
205
213
|
);
|
|
@@ -269,18 +277,72 @@ All requests need \`Authorization: Bearer <jwt>\` header.
|
|
|
269
277
|
await execOrDie(["bun", "install"], { cwd: projectDir });
|
|
270
278
|
log.success("dependencies installed");
|
|
271
279
|
|
|
280
|
+
// ── 7. bootstrap DB + create admin ──
|
|
281
|
+
|
|
282
|
+
if (databaseUrl) {
|
|
283
|
+
// run migrations (bootstrap + user's 001_init.sql which creates users table)
|
|
284
|
+
log.info("running migrations...");
|
|
285
|
+
await exec(["bunx", "@dotenvx/dotenvx", "run", "--", "bunx", "upend", "migrate"], { cwd: projectDir });
|
|
286
|
+
log.success("database ready");
|
|
287
|
+
|
|
288
|
+
// prompt: create admin or enable signup?
|
|
289
|
+
log.blank();
|
|
290
|
+
process.stdout.write(" create an admin user now? (Y/n): ");
|
|
291
|
+
const answer = (await readLine()).trim().toLowerCase();
|
|
292
|
+
|
|
293
|
+
if (answer === "n" || answer === "no") {
|
|
294
|
+
// enable signup so they can create accounts from the dashboard
|
|
295
|
+
log.info("enabling public signup...");
|
|
296
|
+
await setEnvVar(projectDir, "SIGNUP_ENABLED", "true");
|
|
297
|
+
log.success("signup enabled — anyone can create an account");
|
|
298
|
+
} else {
|
|
299
|
+
// create admin user
|
|
300
|
+
let email = adminEmail;
|
|
301
|
+
let password = adminPassword;
|
|
302
|
+
|
|
303
|
+
if (!email) {
|
|
304
|
+
process.stdout.write(" admin email: ");
|
|
305
|
+
email = (await readLine()).trim();
|
|
306
|
+
}
|
|
307
|
+
if (!password) {
|
|
308
|
+
process.stdout.write(" admin password: ");
|
|
309
|
+
password = (await readLine()).trim();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (email && password) {
|
|
313
|
+
log.info("creating admin user...");
|
|
314
|
+
const postgres = (await import("postgres")).default;
|
|
315
|
+
const sql = postgres(databaseUrl, { max: 1 });
|
|
316
|
+
const passwordHash = await Bun.password.hash(password, { algorithm: "argon2id" });
|
|
317
|
+
try {
|
|
318
|
+
const [user] = await sql`
|
|
319
|
+
INSERT INTO users (email, password_hash, role)
|
|
320
|
+
VALUES (${email}, ${passwordHash}, 'admin')
|
|
321
|
+
RETURNING id, email, role
|
|
322
|
+
`;
|
|
323
|
+
log.success(`admin: ${user.email}`);
|
|
324
|
+
} catch (err: any) {
|
|
325
|
+
if (err.code === "23505") {
|
|
326
|
+
log.warn("admin user already exists");
|
|
327
|
+
} else {
|
|
328
|
+
log.warn(`could not create admin: ${err.message}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
await sql.end();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
272
336
|
// ── done ──
|
|
273
337
|
|
|
274
338
|
log.blank();
|
|
275
339
|
log.header(`${name} is ready`);
|
|
276
340
|
log.blank();
|
|
277
341
|
log.info(`cd ${name}`);
|
|
278
|
-
if (!databaseUrl) {
|
|
279
|
-
log.info("# add your DATABASE_URL to .env");
|
|
280
|
-
}
|
|
281
342
|
if (!process.env.ANTHROPIC_API_KEY) {
|
|
282
|
-
log.info("
|
|
343
|
+
log.info("upend env:set ANTHROPIC_API_KEY <your-key>");
|
|
283
344
|
}
|
|
345
|
+
log.info("upend migrate");
|
|
284
346
|
log.info("upend dev");
|
|
285
347
|
log.blank();
|
|
286
348
|
if (databaseUrl) {
|
|
@@ -314,6 +376,34 @@ function writeFile(dir: string, path: string, content: string) {
|
|
|
314
376
|
writeFileSync(fullPath, content);
|
|
315
377
|
}
|
|
316
378
|
|
|
379
|
+
async function setEnvVar(projectDir: string, key: string, value: string) {
|
|
380
|
+
await exec(["bunx", "@dotenvx/dotenvx", "decrypt"], { cwd: projectDir, silent: true });
|
|
381
|
+
const envFile = readFileSync(join(projectDir, ".env"), "utf-8");
|
|
382
|
+
const regex = new RegExp(`^${key}=.*$`, "m");
|
|
383
|
+
const updated = regex.test(envFile)
|
|
384
|
+
? envFile.replace(regex, `${key}=${value}`)
|
|
385
|
+
: envFile.trimEnd() + `\n${key}=${value}\n`;
|
|
386
|
+
writeFileSync(join(projectDir, ".env"), updated);
|
|
387
|
+
await exec(["bunx", "@dotenvx/dotenvx", "encrypt"], { cwd: projectDir, silent: true });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function getFlag(args: string[], flag: string): string | undefined {
|
|
391
|
+
const idx = args.indexOf(flag);
|
|
392
|
+
return idx !== -1 && args[idx + 1] ? args[idx + 1] : undefined;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function readLine(): Promise<string> {
|
|
396
|
+
return new Promise((resolve) => {
|
|
397
|
+
const stdin = process.stdin;
|
|
398
|
+
stdin.resume();
|
|
399
|
+
stdin.setEncoding("utf-8");
|
|
400
|
+
stdin.once("data", (data: string) => {
|
|
401
|
+
stdin.pause();
|
|
402
|
+
resolve(data);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
317
407
|
async function exportKeyToPem(key: CryptoKey, type: "PRIVATE" | "PUBLIC") {
|
|
318
408
|
const format = type === "PRIVATE" ? "pkcs8" : "spki";
|
|
319
409
|
const exported = await crypto.subtle.exportKey(format, key);
|
package/src/commands/migrate.ts
CHANGED
|
@@ -2,13 +2,15 @@ import { log } from "../lib/log";
|
|
|
2
2
|
import { readdirSync, readFileSync } from "fs";
|
|
3
3
|
import { join, resolve } from "path";
|
|
4
4
|
|
|
5
|
+
// bootstrap SQL lives in the package — framework tables
|
|
6
|
+
const bootstrapPath = new URL("../lib/bootstrap.sql", import.meta.url).pathname;
|
|
7
|
+
|
|
5
8
|
export default async function migrate(args: string[]) {
|
|
6
9
|
const projectDir = resolve(".");
|
|
7
10
|
const migrationsDir = join(projectDir, "migrations");
|
|
8
11
|
|
|
9
12
|
log.header("running migrations");
|
|
10
13
|
|
|
11
|
-
// dynamic import postgres from the project's node_modules
|
|
12
14
|
const postgres = (await import("postgres")).default;
|
|
13
15
|
const sql = postgres(process.env.DATABASE_URL!, {
|
|
14
16
|
max: 1,
|
|
@@ -16,14 +18,13 @@ export default async function migrate(args: string[]) {
|
|
|
16
18
|
});
|
|
17
19
|
|
|
18
20
|
try {
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
)
|
|
25
|
-
`;
|
|
21
|
+
// 1. bootstrap framework tables (idempotent)
|
|
22
|
+
log.info("bootstrapping framework tables...");
|
|
23
|
+
const bootstrap = readFileSync(bootstrapPath, "utf-8");
|
|
24
|
+
await sql.unsafe(bootstrap);
|
|
25
|
+
log.success("framework tables ready");
|
|
26
26
|
|
|
27
|
+
// 2. run user migrations
|
|
27
28
|
const ran = new Set(
|
|
28
29
|
(await sql`SELECT name FROM _migrations`).map((r: any) => r.name)
|
|
29
30
|
);
|
|
@@ -35,7 +36,8 @@ export default async function migrate(args: string[]) {
|
|
|
35
36
|
.sort();
|
|
36
37
|
} catch {
|
|
37
38
|
log.warn("no migrations directory found");
|
|
38
|
-
|
|
39
|
+
await sql.end();
|
|
40
|
+
return;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
let count = 0;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- upend framework tables — created automatically, not user-managed
|
|
2
|
+
|
|
3
|
+
-- internal schema (hidden from Data API)
|
|
4
|
+
CREATE SCHEMA IF NOT EXISTS upend;
|
|
5
|
+
|
|
6
|
+
-- migration tracking (public, but prefixed with _ so Data API ignores it)
|
|
7
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
8
|
+
id SERIAL PRIMARY KEY,
|
|
9
|
+
name TEXT UNIQUE NOT NULL,
|
|
10
|
+
applied_at TIMESTAMPTZ DEFAULT now()
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
-- oauth state tracking — internal
|
|
14
|
+
CREATE TABLE IF NOT EXISTS upend.oauth_states (
|
|
15
|
+
id SERIAL PRIMARY KEY,
|
|
16
|
+
state TEXT UNIQUE NOT NULL,
|
|
17
|
+
provider TEXT NOT NULL,
|
|
18
|
+
created_at TIMESTAMPTZ DEFAULT now()
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
-- claude editing sessions — internal
|
|
22
|
+
CREATE TABLE IF NOT EXISTS upend.editing_sessions (
|
|
23
|
+
id BIGSERIAL PRIMARY KEY,
|
|
24
|
+
prompt TEXT NOT NULL,
|
|
25
|
+
title TEXT,
|
|
26
|
+
status TEXT DEFAULT 'active',
|
|
27
|
+
claude_session_id TEXT,
|
|
28
|
+
snapshot_name TEXT,
|
|
29
|
+
context JSONB DEFAULT '{}',
|
|
30
|
+
created_at TIMESTAMPTZ DEFAULT now(),
|
|
31
|
+
updated_at TIMESTAMPTZ DEFAULT now()
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
-- session messages — internal
|
|
35
|
+
CREATE TABLE IF NOT EXISTS upend.session_messages (
|
|
36
|
+
id BIGSERIAL PRIMARY KEY,
|
|
37
|
+
session_id BIGINT REFERENCES upend.editing_sessions(id),
|
|
38
|
+
role TEXT NOT NULL,
|
|
39
|
+
content TEXT,
|
|
40
|
+
result TEXT,
|
|
41
|
+
status TEXT DEFAULT 'pending',
|
|
42
|
+
created_at TIMESTAMPTZ DEFAULT now()
|
|
43
|
+
);
|
package/src/lib/db.ts
CHANGED
|
@@ -10,8 +10,12 @@ authRoutes.get("/.well-known/jwks.json", async (c) => {
|
|
|
10
10
|
return c.json(jwks);
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
-
// signup
|
|
13
|
+
// signup (disabled by default — admin creates users, or set SIGNUP_ENABLED=true)
|
|
14
14
|
authRoutes.post("/auth/signup", async (c) => {
|
|
15
|
+
if (process.env.SIGNUP_ENABLED !== "true") {
|
|
16
|
+
return c.json({ error: "signup is disabled — contact the admin" }, 403);
|
|
17
|
+
}
|
|
18
|
+
|
|
15
19
|
const { email, password, role } = await c.req.json();
|
|
16
20
|
console.log(`[auth] signup attempt: ${email}`);
|
|
17
21
|
if (!email || !password) return c.json({ error: "email and password required" }, 400);
|