@binary1702/auth 0.2.5 → 0.4.0
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/AGENTS.md +155 -0
- package/CHANGELOG.md +49 -0
- package/INSTALL.md +27 -16
- package/PROMPT.md +325 -36
- package/README.md +338 -53
- package/dist/cli.js +248 -17
- package/dist/core/admin-html.d.ts +32 -0
- package/dist/core/admin-html.js +350 -0
- package/dist/core/admin.d.ts +56 -0
- package/dist/core/admin.js +90 -0
- package/dist/core/config.d.ts +10 -3
- package/dist/core/config.js +22 -8
- package/dist/core/db.d.ts +155 -10
- package/dist/core/db.js +449 -69
- package/dist/core/email.d.ts +49 -0
- package/dist/core/email.js +213 -0
- package/dist/core/guard.d.ts +52 -0
- package/dist/core/guard.js +106 -0
- package/dist/core/html.d.ts +43 -0
- package/dist/core/html.js +122 -28
- package/dist/core/invite.d.ts +7 -0
- package/dist/core/invite.js +30 -0
- package/dist/core/options.d.ts +95 -0
- package/dist/core/options.js +377 -0
- package/dist/core/org.d.ts +43 -0
- package/dist/core/org.js +65 -0
- package/dist/core/pages.d.ts +34 -0
- package/dist/core/pages.js +73 -0
- package/dist/core/password.d.ts +16 -0
- package/dist/core/password.js +41 -12
- package/dist/core/rate.d.ts +26 -0
- package/dist/core/rate.js +70 -0
- package/dist/core/reset.d.ts +9 -0
- package/dist/core/reset.js +31 -0
- package/dist/core/session.d.ts +11 -1
- package/dist/core/session.js +27 -7
- package/dist/core/signup.d.ts +13 -6
- package/dist/core/signup.js +30 -15
- package/dist/core/tokens.d.ts +29 -0
- package/dist/core/tokens.js +64 -0
- package/dist/core/verify.d.ts +8 -0
- package/dist/core/verify.js +33 -0
- package/dist/core/yaml.d.ts +10 -0
- package/dist/core/yaml.js +85 -0
- package/dist/index.d.ts +18 -6
- package/dist/index.js +18 -6
- package/dist/next/index.d.ts +10 -3
- package/dist/next/index.js +649 -63
- package/dist/next/proxy.d.ts +2 -1
- package/dist/next/proxy.js +22 -10
- package/dist/react/index.d.ts +72 -0
- package/dist/react/index.js +107 -0
- package/package.json +18 -4
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// 3. AUTH_SECRET → generate, write .env.local
|
|
11
11
|
// 4. app/auth/[...path]/route.ts
|
|
12
12
|
// 5. proxy.ts
|
|
13
|
-
// 6. create
|
|
13
|
+
// 6. create users table
|
|
14
14
|
// 7. create the first user
|
|
15
15
|
//
|
|
16
16
|
// Subcommands:
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
// remove-user [email] delete a user (signs them out everywhere)
|
|
20
20
|
// sign-out-everywhere [email] end every session of a user
|
|
21
21
|
// list-users
|
|
22
|
+
// add-org <name> create an organization
|
|
23
|
+
// add-member <email> <org> [role] put a user in an org (role defaults to "member")
|
|
24
|
+
// remove-member <email> <org>
|
|
25
|
+
// list-orgs
|
|
22
26
|
// ─────────────────────────────────────────────────────────────
|
|
23
27
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
|
|
24
28
|
import { join, dirname } from "node:path";
|
|
@@ -77,7 +81,7 @@ function closePrompts() {
|
|
|
77
81
|
}
|
|
78
82
|
async function ask(question, opts = {}) {
|
|
79
83
|
rl();
|
|
80
|
-
const suffix = opts.default ? c.dim(` (${opts.default})`) : "";
|
|
84
|
+
const suffix = opts.default ? c.dim(` (${opts.default})`) : opts.optional ? c.dim(" (Enter to skip)") : "";
|
|
81
85
|
stdout.write(` ${question}${suffix}: `);
|
|
82
86
|
if (opts.hidden)
|
|
83
87
|
muted = true;
|
|
@@ -90,8 +94,12 @@ async function ask(question, opts = {}) {
|
|
|
90
94
|
muted = false;
|
|
91
95
|
stdout.write("\n");
|
|
92
96
|
}
|
|
93
|
-
if (line === null)
|
|
97
|
+
if (line === null) {
|
|
98
|
+
// Piped input that stops short: an optional prompt is simply skipped
|
|
99
|
+
if (opts.optional)
|
|
100
|
+
return "";
|
|
94
101
|
return fail("Input ended before all prompts were answered.");
|
|
102
|
+
}
|
|
95
103
|
return opts.hidden ? line : line.trim() || opts.default || "";
|
|
96
104
|
}
|
|
97
105
|
// ── env file ──────────────────────────────────────────────────
|
|
@@ -118,11 +126,14 @@ const PROXY_FILE = `import { proxy } from "${PKG}/next/proxy"
|
|
|
118
126
|
|
|
119
127
|
export default proxy
|
|
120
128
|
|
|
121
|
-
// Everything except
|
|
129
|
+
// Everything except Next's build output. The proxy itself lets the
|
|
130
|
+
// auth pages and static files through.
|
|
122
131
|
export const config = {
|
|
123
|
-
matcher: ["/((?!
|
|
132
|
+
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
124
133
|
}
|
|
125
134
|
`;
|
|
135
|
+
// Written by 0.2.5 and earlier: skipped /authors, /api/export.csv, …
|
|
136
|
+
const OLD_MATCHER = "(?!auth|_next/static|_next/image|favicon.ico|.*";
|
|
126
137
|
function writeIfMissing(rel, content) {
|
|
127
138
|
const abs = join(cwd, rel);
|
|
128
139
|
if (existsSync(abs)) {
|
|
@@ -156,7 +167,7 @@ function stepInstall() {
|
|
|
156
167
|
fail(`npm install ${PKG} failed`);
|
|
157
168
|
done(`${PKG} installed`);
|
|
158
169
|
}
|
|
159
|
-
async function stepEnv() {
|
|
170
|
+
async function stepEnv(opts = {}) {
|
|
160
171
|
const env = readEnv();
|
|
161
172
|
if (env.DATABASE_URL) {
|
|
162
173
|
process.env.DATABASE_URL = env.DATABASE_URL;
|
|
@@ -178,12 +189,88 @@ async function stepEnv() {
|
|
|
178
189
|
writeEnv("AUTH_SECRET", randomBytes(32).toString("base64url"));
|
|
179
190
|
done("AUTH_SECRET generated → .env.local");
|
|
180
191
|
}
|
|
192
|
+
// Email: verification and password-reset links. Optional. Without it,
|
|
193
|
+
// development prints the emails to the console and production refuses
|
|
194
|
+
// to send, so the install still works. See core/email.ts.
|
|
195
|
+
for (const k of ["AUTH_GMAIL_USER", "AUTH_GMAIL_CLIENT_EMAIL", "AUTH_GMAIL_PRIVATE_KEY", "AUTH_RESEND_API_KEY", "AUTH_EMAIL_FROM"]) {
|
|
196
|
+
if (env[k])
|
|
197
|
+
process.env[k] = env[k];
|
|
198
|
+
}
|
|
199
|
+
if (env.AUTH_GMAIL_USER && env.AUTH_GMAIL_CLIENT_EMAIL && env.AUTH_GMAIL_PRIVATE_KEY) {
|
|
200
|
+
if (opts.askEmail)
|
|
201
|
+
skip("email: Gmail API as " + env.AUTH_GMAIL_USER);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (env.AUTH_RESEND_API_KEY && env.AUTH_EMAIL_FROM) {
|
|
205
|
+
if (opts.askEmail)
|
|
206
|
+
skip("email: Resend from " + env.AUTH_EMAIL_FROM);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (!opts.askEmail)
|
|
210
|
+
return; // subcommands only need the database
|
|
211
|
+
// Once b17_auth.yaml exists, email is configured there (its comments show how); stop asking
|
|
212
|
+
if (existsSync(join(cwd, CONFIG_FILE)))
|
|
213
|
+
return;
|
|
214
|
+
console.log(c.dim(" Email for verification and password reset. gmail = Gmail API with a service account,"));
|
|
215
|
+
console.log(c.dim(" resend = resend.com API key. Skip it for now if you like."));
|
|
216
|
+
const which = (await ask("Email transport (gmail / resend)", { optional: true })).toLowerCase();
|
|
217
|
+
if (which === "gmail") {
|
|
218
|
+
const user = await ask("Mailbox to send as (AUTH_GMAIL_USER)", { default: env.AUTH_GMAIL_USER });
|
|
219
|
+
if (!user.includes("@"))
|
|
220
|
+
fail("That does not look like an email address.");
|
|
221
|
+
const path = await ask("Path to the service account's JSON key file");
|
|
222
|
+
const abs = path.startsWith("/") ? path : join(cwd, path);
|
|
223
|
+
if (!existsSync(abs))
|
|
224
|
+
fail(`No file at ${abs}`);
|
|
225
|
+
let sa;
|
|
226
|
+
try {
|
|
227
|
+
sa = JSON.parse(readFileSync(abs, "utf8"));
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return fail("That file is not JSON.");
|
|
231
|
+
}
|
|
232
|
+
if (!sa.client_email || !sa.private_key)
|
|
233
|
+
fail("That JSON has no client_email / private_key: it is not a service account key.");
|
|
234
|
+
writeEnv("AUTH_GMAIL_USER", user);
|
|
235
|
+
writeEnv("AUTH_GMAIL_CLIENT_EMAIL", sa.client_email);
|
|
236
|
+
// One line: the PEM's newlines as the two characters \n, which the package restores
|
|
237
|
+
writeEnv("AUTH_GMAIL_PRIVATE_KEY", sa.private_key.replace(/\n/g, "\\n"));
|
|
238
|
+
done(`AUTH_GMAIL_USER, AUTH_GMAIL_CLIENT_EMAIL, AUTH_GMAIL_PRIVATE_KEY → .env.local (${sa.client_email})`);
|
|
239
|
+
console.log(c.dim(` In Google Admin, grant that service account domain-wide delegation for the scope:`));
|
|
240
|
+
console.log(c.dim(` https://www.googleapis.com/auth/gmail.send`));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (which === "resend") {
|
|
244
|
+
const key = env.AUTH_RESEND_API_KEY || (await ask("Resend API key (AUTH_RESEND_API_KEY)", { hidden: true }));
|
|
245
|
+
if (!key.trim())
|
|
246
|
+
fail("No key given.");
|
|
247
|
+
const from = env.AUTH_EMAIL_FROM || (await ask("From address (AUTH_EMAIL_FROM)", { default: "Auth <onboarding@resend.dev>" }));
|
|
248
|
+
if (!env.AUTH_RESEND_API_KEY)
|
|
249
|
+
writeEnv("AUTH_RESEND_API_KEY", key.trim());
|
|
250
|
+
if (!env.AUTH_EMAIL_FROM)
|
|
251
|
+
writeEnv("AUTH_EMAIL_FROM", from);
|
|
252
|
+
done("AUTH_RESEND_API_KEY, AUTH_EMAIL_FROM → .env.local");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (which)
|
|
256
|
+
fail(`Unknown transport "${which}". Use gmail, resend, or press Enter to skip.`);
|
|
257
|
+
skip("email not configured: dev prints emails to the console");
|
|
181
258
|
}
|
|
182
259
|
function stepFiles(appDir, src) {
|
|
183
260
|
writeIfMissing(`${appDir}/auth/[...path]/route.ts`, ROUTE_FILE);
|
|
184
261
|
const proxyPath = src ? "src/proxy.ts" : "proxy.ts";
|
|
185
262
|
const legacy = [src ? "src/middleware.ts" : "middleware.ts", proxyPath].filter((p) => existsSync(join(cwd, p)));
|
|
186
263
|
if (legacy.length) {
|
|
264
|
+
const text = readFileSync(join(cwd, legacy[0]), "utf8");
|
|
265
|
+
if (text.includes(`${PKG}/next/proxy`) && !text.includes(OLD_MATCHER)) {
|
|
266
|
+
skip(`${legacy[0]} exists`); // ours, current: nothing to say
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (text.includes(OLD_MATCHER)) {
|
|
270
|
+
console.log(` ${c.red("!")} ${legacy[0]} has the old matcher, which leaves /auth* and dotted paths unguarded.`);
|
|
271
|
+
console.log(` Replace its matcher with: ${c.bold('["/((?!_next/static|_next/image|favicon.ico).*)"]')}`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
187
274
|
skip(`${legacy[0]} exists, not touched. Add to it:`);
|
|
188
275
|
console.log(c.dim(PROXY_FILE.replace(/^/gm, " ")));
|
|
189
276
|
return;
|
|
@@ -194,10 +281,109 @@ async function db() {
|
|
|
194
281
|
// Imported lazily: needs DATABASE_URL in process.env first.
|
|
195
282
|
return import("./core/db.js");
|
|
196
283
|
}
|
|
284
|
+
// ── b17_auth.yaml ─────────────────────────────────────────────
|
|
285
|
+
const CONFIG_FILE = "b17_auth.yaml";
|
|
286
|
+
function appName() {
|
|
287
|
+
try {
|
|
288
|
+
const name = String(JSON.parse(readFileSync(join(cwd, "package.json"), "utf8")).name ?? "");
|
|
289
|
+
return name.replace(/^@[^/]+\//, "").replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return "My App";
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/** The file the install writes once. Secrets are ${…} references into .env.local. */
|
|
296
|
+
function configTemplate(env) {
|
|
297
|
+
const gmail = env.AUTH_GMAIL_USER && env.AUTH_GMAIL_CLIENT_EMAIL && env.AUTH_GMAIL_PRIVATE_KEY;
|
|
298
|
+
const resend = env.AUTH_RESEND_API_KEY;
|
|
299
|
+
const from = env.AUTH_EMAIL_FROM || (gmail ? env.AUTH_GMAIL_USER : "");
|
|
300
|
+
return `# ${CONFIG_FILE} · @binary1702/auth
|
|
301
|
+
# One place for all of it. Secrets stay in .env.local and appear here as \${NAME}.
|
|
302
|
+
# Docs: https://www.npmjs.com/package/@binary1702/auth#b17_authyaml
|
|
303
|
+
|
|
304
|
+
name: ${appName()} # page titles, email subjects
|
|
305
|
+
signup: closed # open | closed | invite (invite also needs inviteCode)
|
|
306
|
+
# inviteCode: \${AUTH_INVITE_CODE}
|
|
307
|
+
# emailDomain: example.com # only these addresses may sign up
|
|
308
|
+
# requireVerified: true # no session until the email link is clicked
|
|
309
|
+
# protected: /app/* /api/* # only these need sign-in; everything else is open. * = anything below
|
|
310
|
+
# # (or the reverse: public: / /pricing = everything needs sign-in except these)
|
|
311
|
+
# admins: you@example.com # who may open /auth/admin (users, organizations, roles)
|
|
312
|
+
# home: /app # the signed-in home: after sign-in, and where a signed-in visitor to / goes
|
|
313
|
+
# pages: # your own sign-in / sign-up pages instead of the built-in ones;
|
|
314
|
+
# signIn: /sign-in # they call signIn() / signUp() from @binary1702/auth/react
|
|
315
|
+
# signUp: /sign-up
|
|
316
|
+
|
|
317
|
+
database: \${DATABASE_URL}
|
|
318
|
+
secret: \${AUTH_SECRET}
|
|
319
|
+
|
|
320
|
+
email:
|
|
321
|
+
${from ? ` from: ${from}` : " # from: NoReply (My App) <no-reply@example.com>"}
|
|
322
|
+
# replyTo: support@example.com
|
|
323
|
+
${gmail
|
|
324
|
+
? ` gmail:
|
|
325
|
+
user: ${env.AUTH_GMAIL_USER} # the mailbox the service account acts as
|
|
326
|
+
clientEmail: \${AUTH_GMAIL_CLIENT_EMAIL}
|
|
327
|
+
privateKey: \${AUTH_GMAIL_PRIVATE_KEY}`
|
|
328
|
+
: resend
|
|
329
|
+
? ` resend:
|
|
330
|
+
apiKey: \${AUTH_RESEND_API_KEY}`
|
|
331
|
+
: ` # gmail: # one of the two; see the README
|
|
332
|
+
# user: you@example.com
|
|
333
|
+
# clientEmail: \${AUTH_GMAIL_CLIENT_EMAIL}
|
|
334
|
+
# privateKey: \${AUTH_GMAIL_PRIVATE_KEY}
|
|
335
|
+
# resend:
|
|
336
|
+
# apiKey: \${AUTH_RESEND_API_KEY}`}
|
|
337
|
+
|
|
338
|
+
# permissions: # what the app gates: id: label. The rows on /auth/admin › Roles
|
|
339
|
+
# invoices.read: See invoices
|
|
340
|
+
# invoices.create: Create invoices
|
|
341
|
+
|
|
342
|
+
roles: # role: what it may do to start with. all = everything, x.* = a prefix
|
|
343
|
+
owner: all # (/auth/admin › Roles saves changes in the database)
|
|
344
|
+
admin: all
|
|
345
|
+
member:
|
|
346
|
+
`;
|
|
347
|
+
}
|
|
348
|
+
async function stepConfig() {
|
|
349
|
+
const abs = join(cwd, CONFIG_FILE);
|
|
350
|
+
if (existsSync(abs)) {
|
|
351
|
+
const { parseYaml } = await import("./core/yaml.js");
|
|
352
|
+
const { literalSecrets } = await import("./core/options.js");
|
|
353
|
+
let bad = [];
|
|
354
|
+
try {
|
|
355
|
+
bad = literalSecrets(parseYaml(readFileSync(abs, "utf8")));
|
|
356
|
+
}
|
|
357
|
+
catch (e) {
|
|
358
|
+
fail(`${CONFIG_FILE}: ${e instanceof Error ? e.message : String(e)}`);
|
|
359
|
+
}
|
|
360
|
+
if (bad.length) {
|
|
361
|
+
const ignored = spawnSync("git", ["check-ignore", "-q", CONFIG_FILE], { cwd }).status === 0;
|
|
362
|
+
if (!ignored) {
|
|
363
|
+
fail(`${CONFIG_FILE} holds a literal secret under ${bad.join(", ")}. Move it to .env.local and reference it as \${NAME}, or git-ignore the file.`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
skip(`${CONFIG_FILE} exists`);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
writeFileSync(abs, configTemplate(readEnv()));
|
|
370
|
+
done(`${CONFIG_FILE} written: app name, sign-up policy, email, permissions`);
|
|
371
|
+
}
|
|
372
|
+
/** The roles from b17_auth.yaml, written into the database once. A role the admin page already saved is left alone. */
|
|
373
|
+
async function stepRoles() {
|
|
374
|
+
const { seedRoles } = await db();
|
|
375
|
+
const { options, resetOptions } = await import("./core/options.js");
|
|
376
|
+
resetOptions(); // the file may have been written a step ago
|
|
377
|
+
const written = await seedRoles(options().roles);
|
|
378
|
+
if (written.length)
|
|
379
|
+
done(`roles written to the database: ${written.join(", ")} ${c.dim("(the starting point; /auth/admin › Roles decides from here)")}`);
|
|
380
|
+
else
|
|
381
|
+
skip("roles already in the database");
|
|
382
|
+
}
|
|
197
383
|
async function stepSchema() {
|
|
198
|
-
const { ensureSchema } = await db();
|
|
384
|
+
const { ensureSchema, MIGRATIONS } = await db();
|
|
199
385
|
await ensureSchema().catch((e) => fail(`Could not reach the database: ${e.message}`));
|
|
200
|
-
done(
|
|
386
|
+
done(`database schema ready (${MIGRATIONS.length} migration${MIGRATIONS.length === 1 ? "" : "s"})`);
|
|
201
387
|
}
|
|
202
388
|
async function createUserInteractive(presetEmail) {
|
|
203
389
|
const { createUser, findUser } = await db();
|
|
@@ -213,7 +399,8 @@ async function createUserInteractive(presetEmail) {
|
|
|
213
399
|
const again = await ask("Password again", { hidden: true });
|
|
214
400
|
if (password !== again)
|
|
215
401
|
fail("Passwords do not match.");
|
|
216
|
-
|
|
402
|
+
// Created by whoever runs the CLI: no verification email to wait for
|
|
403
|
+
await createUser(email, await hashPassword(password), { verified: true });
|
|
217
404
|
done(`user "${email}" created`);
|
|
218
405
|
}
|
|
219
406
|
async function stepFirstUser() {
|
|
@@ -230,15 +417,15 @@ function finish() {
|
|
|
230
417
|
console.log(`
|
|
231
418
|
${c.bold("Done.")} Start the dev server and open ${c.bold("/auth/sign-in")}.
|
|
232
419
|
|
|
233
|
-
Get the
|
|
420
|
+
Get the session in any route, server component or action:
|
|
234
421
|
|
|
235
|
-
import {
|
|
236
|
-
const
|
|
422
|
+
import { getSession, requirePermission } from "${PKG}/next"
|
|
423
|
+
const session = await getSession() // { user, organization, authorization } or null
|
|
237
424
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
425
|
+
Settings live in ${c.bold(CONFIG_FILE)}: app name, who may sign up (signup: open | invite),
|
|
426
|
+
which paths need sign-in (protected:), email, roles → permissions. Secrets stay in .env.local.
|
|
427
|
+
Add users later: npx ${PKG} add-user Organizations: npx ${PKG} add-org <name>
|
|
428
|
+
Working with an AI agent in this app? Point it at node_modules/${PKG}/AGENTS.md
|
|
242
429
|
`);
|
|
243
430
|
}
|
|
244
431
|
// ── main ──────────────────────────────────────────────────────
|
|
@@ -252,11 +439,53 @@ async function main() {
|
|
|
252
439
|
npx ${PKG} remove-user [email] delete; also signs them out everywhere
|
|
253
440
|
npx ${PKG} sign-out-everywhere [email] end every session of a user
|
|
254
441
|
npx ${PKG} list-users
|
|
442
|
+
|
|
443
|
+
npx ${PKG} add-org <name> create an organization
|
|
444
|
+
npx ${PKG} add-member <email> <org> [role] add a user to an org (role: member, admin, owner, …)
|
|
445
|
+
npx ${PKG} remove-member <email> <org>
|
|
446
|
+
npx ${PKG} list-orgs
|
|
255
447
|
`);
|
|
256
448
|
return;
|
|
257
449
|
}
|
|
258
450
|
stepProject();
|
|
259
451
|
const USER_CMDS = ["add-user", "set-password", "remove-user", "sign-out-everywhere", "list-users"];
|
|
452
|
+
const ORG_CMDS = ["add-org", "add-member", "remove-member", "list-orgs"];
|
|
453
|
+
if (ORG_CMDS.includes(cmd)) {
|
|
454
|
+
await stepEnv();
|
|
455
|
+
const d = await db();
|
|
456
|
+
const [, email, orgName, role] = process.argv.slice(2);
|
|
457
|
+
const org = async (name) => {
|
|
458
|
+
if (!name)
|
|
459
|
+
fail(`Usage: npx ${PKG} ${cmd} <email> <org>`);
|
|
460
|
+
return (await d.findOrg(name)) ?? fail(`No organization "${name}". Create it: npx ${PKG} add-org "${name}"`);
|
|
461
|
+
};
|
|
462
|
+
const user = async (e) => {
|
|
463
|
+
if (!e)
|
|
464
|
+
fail(`Usage: npx ${PKG} ${cmd} <email> <org>`);
|
|
465
|
+
return (await d.findUser(e)) ?? fail(`No user "${e}".`);
|
|
466
|
+
};
|
|
467
|
+
if (cmd === "add-org") {
|
|
468
|
+
if (!arg)
|
|
469
|
+
fail(`Usage: npx ${PKG} add-org <name>`);
|
|
470
|
+
const o = await d.createOrg(arg);
|
|
471
|
+
done(`organization "${o.name}" created ${c.dim(o.id)}`);
|
|
472
|
+
}
|
|
473
|
+
if (cmd === "list-orgs") {
|
|
474
|
+
for (const o of await d.listOrgs())
|
|
475
|
+
console.log(` ${o.name} ${c.dim(o.id)} ${o.members} member${o.members === 1 ? "" : "s"}`);
|
|
476
|
+
}
|
|
477
|
+
if (cmd === "add-member") {
|
|
478
|
+
const [u, o] = [await user(email), await org(orgName)];
|
|
479
|
+
await d.addMember(o.id, u.id, role || "member");
|
|
480
|
+
done(`"${u.email}" is ${role || "member"} of "${o.name}"`);
|
|
481
|
+
}
|
|
482
|
+
if (cmd === "remove-member") {
|
|
483
|
+
const [u, o] = [await user(email), await org(orgName)];
|
|
484
|
+
(await d.removeMember(o.id, u.id)) ? done(`"${u.email}" removed from "${o.name}"`) : fail(`"${u.email}" is not a member of "${o.name}".`);
|
|
485
|
+
}
|
|
486
|
+
console.log();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
260
489
|
if (USER_CMDS.includes(cmd)) {
|
|
261
490
|
await stepEnv();
|
|
262
491
|
const d = await db();
|
|
@@ -295,9 +524,11 @@ async function main() {
|
|
|
295
524
|
fail(`Unknown command "${cmd}". Try: npx ${PKG} help`);
|
|
296
525
|
const { appDir, src } = stepProject();
|
|
297
526
|
stepInstall();
|
|
298
|
-
await stepEnv();
|
|
527
|
+
await stepEnv({ askEmail: true });
|
|
299
528
|
stepFiles(appDir, src);
|
|
529
|
+
await stepConfig();
|
|
300
530
|
await stepSchema();
|
|
531
|
+
await stepRoles();
|
|
301
532
|
await stepFirstUser();
|
|
302
533
|
finish();
|
|
303
534
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AdminOrg, AdminUser, StoredRole } from "./db.ts";
|
|
2
|
+
import type { Options } from "./options.ts";
|
|
3
|
+
export type AdminView = {
|
|
4
|
+
tab: "orgs" | "users" | "roles" | "settings";
|
|
5
|
+
} | {
|
|
6
|
+
tab: "org";
|
|
7
|
+
orgId: string;
|
|
8
|
+
};
|
|
9
|
+
export type AdminPageData = {
|
|
10
|
+
self: string;
|
|
11
|
+
view: AdminView;
|
|
12
|
+
users: AdminUser[];
|
|
13
|
+
orgs: AdminOrg[];
|
|
14
|
+
roles: string[];
|
|
15
|
+
/** what the admin page saved for roles, all scopes */
|
|
16
|
+
stored: StoredRole[];
|
|
17
|
+
options: Options;
|
|
18
|
+
transport: string;
|
|
19
|
+
tokens: {
|
|
20
|
+
total: number;
|
|
21
|
+
live: number;
|
|
22
|
+
};
|
|
23
|
+
/** an organization's owner, not a system admin: only their organization is shown */
|
|
24
|
+
limited?: boolean;
|
|
25
|
+
notice?: string;
|
|
26
|
+
error?: string;
|
|
27
|
+
/** a one-time link (invite or verify) just issued, shown so the admin can hand it over */
|
|
28
|
+
link?: string;
|
|
29
|
+
/** per-render, for the one script the page carries; must match the CSP header */
|
|
30
|
+
nonce: string;
|
|
31
|
+
};
|
|
32
|
+
export declare function adminPage(d: AdminPageData): string;
|