@binary1702/auth 0.2.4 → 0.3.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 +139 -0
- package/CHANGELOG.md +31 -0
- package/INSTALL.md +30 -17
- package/PROMPT.md +325 -37
- package/README.md +293 -56
- package/dist/cli.js +255 -22
- package/dist/core/admin-html.d.ts +26 -0
- package/dist/core/admin-html.js +252 -0
- package/dist/core/admin.d.ts +26 -0
- package/dist/core/admin.js +48 -0
- package/dist/core/config.d.ts +8 -1
- package/dist/core/config.js +20 -6
- package/dist/core/db.d.ts +138 -7
- package/dist/core/db.js +406 -47
- package/dist/core/email.d.ts +49 -0
- package/dist/core/email.js +213 -0
- package/dist/core/guard.d.ts +49 -0
- package/dist/core/guard.js +103 -0
- package/dist/core/html.d.ts +36 -0
- package/dist/core/html.js +106 -25
- package/dist/core/options.d.ts +76 -0
- package/dist/core/options.js +310 -0
- package/dist/core/org.d.ts +34 -0
- package/dist/core/org.js +55 -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 +23 -2
- package/dist/core/session.js +45 -7
- package/dist/core/signup.d.ts +9 -6
- package/dist/core/signup.js +26 -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 +17 -6
- package/dist/index.js +17 -6
- package/dist/next/index.d.ts +11 -4
- package/dist/next/index.js +455 -51
- package/dist/next/proxy.d.ts +2 -1
- package/dist/next/proxy.js +16 -7
- package/dist/react/index.d.ts +18 -0
- package/dist/react/index.js +55 -0
- package/package.json +16 -3
package/dist/cli.js
CHANGED
|
@@ -10,13 +10,19 @@
|
|
|
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:
|
|
17
|
-
// add-user [
|
|
18
|
-
// set-password [
|
|
17
|
+
// add-user [email] create another user
|
|
18
|
+
// set-password [email] change a password (signs them out everywhere)
|
|
19
|
+
// remove-user [email] delete a user (signs them out everywhere)
|
|
20
|
+
// sign-out-everywhere [email] end every session of a user
|
|
19
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
|
|
20
26
|
// ─────────────────────────────────────────────────────────────
|
|
21
27
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
|
|
22
28
|
import { join, dirname } from "node:path";
|
|
@@ -75,7 +81,7 @@ function closePrompts() {
|
|
|
75
81
|
}
|
|
76
82
|
async function ask(question, opts = {}) {
|
|
77
83
|
rl();
|
|
78
|
-
const suffix = opts.default ? c.dim(` (${opts.default})`) : "";
|
|
84
|
+
const suffix = opts.default ? c.dim(` (${opts.default})`) : opts.optional ? c.dim(" (Enter to skip)") : "";
|
|
79
85
|
stdout.write(` ${question}${suffix}: `);
|
|
80
86
|
if (opts.hidden)
|
|
81
87
|
muted = true;
|
|
@@ -88,8 +94,12 @@ async function ask(question, opts = {}) {
|
|
|
88
94
|
muted = false;
|
|
89
95
|
stdout.write("\n");
|
|
90
96
|
}
|
|
91
|
-
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 "";
|
|
92
101
|
return fail("Input ended before all prompts were answered.");
|
|
102
|
+
}
|
|
93
103
|
return opts.hidden ? line : line.trim() || opts.default || "";
|
|
94
104
|
}
|
|
95
105
|
// ── env file ──────────────────────────────────────────────────
|
|
@@ -116,11 +126,14 @@ const PROXY_FILE = `import { proxy } from "${PKG}/next/proxy"
|
|
|
116
126
|
|
|
117
127
|
export default proxy
|
|
118
128
|
|
|
119
|
-
// Everything except
|
|
129
|
+
// Everything except Next's build output. The proxy itself lets the
|
|
130
|
+
// auth pages and static files through.
|
|
120
131
|
export const config = {
|
|
121
|
-
matcher: ["/((?!
|
|
132
|
+
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
122
133
|
}
|
|
123
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|.*";
|
|
124
137
|
function writeIfMissing(rel, content) {
|
|
125
138
|
const abs = join(cwd, rel);
|
|
126
139
|
if (existsSync(abs)) {
|
|
@@ -154,7 +167,7 @@ function stepInstall() {
|
|
|
154
167
|
fail(`npm install ${PKG} failed`);
|
|
155
168
|
done(`${PKG} installed`);
|
|
156
169
|
}
|
|
157
|
-
async function stepEnv() {
|
|
170
|
+
async function stepEnv(opts = {}) {
|
|
158
171
|
const env = readEnv();
|
|
159
172
|
if (env.DATABASE_URL) {
|
|
160
173
|
process.env.DATABASE_URL = env.DATABASE_URL;
|
|
@@ -176,12 +189,88 @@ async function stepEnv() {
|
|
|
176
189
|
writeEnv("AUTH_SECRET", randomBytes(32).toString("base64url"));
|
|
177
190
|
done("AUTH_SECRET generated → .env.local");
|
|
178
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");
|
|
179
258
|
}
|
|
180
259
|
function stepFiles(appDir, src) {
|
|
181
260
|
writeIfMissing(`${appDir}/auth/[...path]/route.ts`, ROUTE_FILE);
|
|
182
261
|
const proxyPath = src ? "src/proxy.ts" : "proxy.ts";
|
|
183
262
|
const legacy = [src ? "src/middleware.ts" : "middleware.ts", proxyPath].filter((p) => existsSync(join(cwd, p)));
|
|
184
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
|
+
}
|
|
185
274
|
skip(`${legacy[0]} exists, not touched. Add to it:`);
|
|
186
275
|
console.log(c.dim(PROXY_FILE.replace(/^/gm, " ")));
|
|
187
276
|
return;
|
|
@@ -192,10 +281,94 @@ async function db() {
|
|
|
192
281
|
// Imported lazily: needs DATABASE_URL in process.env first.
|
|
193
282
|
return import("./core/db.js");
|
|
194
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
|
+
|
|
313
|
+
database: \${DATABASE_URL}
|
|
314
|
+
secret: \${AUTH_SECRET}
|
|
315
|
+
|
|
316
|
+
email:
|
|
317
|
+
${from ? ` from: ${from}` : " # from: NoReply (My App) <no-reply@example.com>"}
|
|
318
|
+
# replyTo: support@example.com
|
|
319
|
+
${gmail
|
|
320
|
+
? ` gmail:
|
|
321
|
+
user: ${env.AUTH_GMAIL_USER} # the mailbox the service account acts as
|
|
322
|
+
clientEmail: \${AUTH_GMAIL_CLIENT_EMAIL}
|
|
323
|
+
privateKey: \${AUTH_GMAIL_PRIVATE_KEY}`
|
|
324
|
+
: resend
|
|
325
|
+
? ` resend:
|
|
326
|
+
apiKey: \${AUTH_RESEND_API_KEY}`
|
|
327
|
+
: ` # gmail: # one of the two; see the README
|
|
328
|
+
# user: you@example.com
|
|
329
|
+
# clientEmail: \${AUTH_GMAIL_CLIENT_EMAIL}
|
|
330
|
+
# privateKey: \${AUTH_GMAIL_PRIVATE_KEY}
|
|
331
|
+
# resend:
|
|
332
|
+
# apiKey: \${AUTH_RESEND_API_KEY}`}
|
|
333
|
+
|
|
334
|
+
permissions: # role: what it may do. all = everything, x.* = a prefix
|
|
335
|
+
owner: all
|
|
336
|
+
admin: all
|
|
337
|
+
member:
|
|
338
|
+
|
|
339
|
+
# organizations: # overrides by organization name
|
|
340
|
+
# Acme:
|
|
341
|
+
# member: invoices.read
|
|
342
|
+
`;
|
|
343
|
+
}
|
|
344
|
+
async function stepConfig() {
|
|
345
|
+
const abs = join(cwd, CONFIG_FILE);
|
|
346
|
+
if (existsSync(abs)) {
|
|
347
|
+
const { parseYaml } = await import("./core/yaml.js");
|
|
348
|
+
const { literalSecrets } = await import("./core/options.js");
|
|
349
|
+
let bad = [];
|
|
350
|
+
try {
|
|
351
|
+
bad = literalSecrets(parseYaml(readFileSync(abs, "utf8")));
|
|
352
|
+
}
|
|
353
|
+
catch (e) {
|
|
354
|
+
fail(`${CONFIG_FILE}: ${e instanceof Error ? e.message : String(e)}`);
|
|
355
|
+
}
|
|
356
|
+
if (bad.length) {
|
|
357
|
+
const ignored = spawnSync("git", ["check-ignore", "-q", CONFIG_FILE], { cwd }).status === 0;
|
|
358
|
+
if (!ignored) {
|
|
359
|
+
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.`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
skip(`${CONFIG_FILE} exists`);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
writeFileSync(abs, configTemplate(readEnv()));
|
|
366
|
+
done(`${CONFIG_FILE} written: app name, sign-up policy, email, permissions`);
|
|
367
|
+
}
|
|
195
368
|
async function stepSchema() {
|
|
196
|
-
const { ensureSchema } = await db();
|
|
369
|
+
const { ensureSchema, MIGRATIONS } = await db();
|
|
197
370
|
await ensureSchema().catch((e) => fail(`Could not reach the database: ${e.message}`));
|
|
198
|
-
done(
|
|
371
|
+
done(`database schema ready (${MIGRATIONS.length} migration${MIGRATIONS.length === 1 ? "" : "s"})`);
|
|
199
372
|
}
|
|
200
373
|
async function createUserInteractive(presetEmail) {
|
|
201
374
|
const { createUser, findUser } = await db();
|
|
@@ -211,7 +384,8 @@ async function createUserInteractive(presetEmail) {
|
|
|
211
384
|
const again = await ask("Password again", { hidden: true });
|
|
212
385
|
if (password !== again)
|
|
213
386
|
fail("Passwords do not match.");
|
|
214
|
-
|
|
387
|
+
// Created by whoever runs the CLI: no verification email to wait for
|
|
388
|
+
await createUser(email, await hashPassword(password), { verified: true });
|
|
215
389
|
done(`user "${email}" created`);
|
|
216
390
|
}
|
|
217
391
|
async function stepFirstUser() {
|
|
@@ -228,15 +402,15 @@ function finish() {
|
|
|
228
402
|
console.log(`
|
|
229
403
|
${c.bold("Done.")} Start the dev server and open ${c.bold("/auth/sign-in")}.
|
|
230
404
|
|
|
231
|
-
Get the
|
|
405
|
+
Get the session in any route, server component or action:
|
|
232
406
|
|
|
233
|
-
import {
|
|
234
|
-
const
|
|
407
|
+
import { getSession, requirePermission } from "${PKG}/next"
|
|
408
|
+
const session = await getSession() // { user, organization, authorization } or null
|
|
235
409
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
410
|
+
Settings live in ${c.bold(CONFIG_FILE)}: app name, who may sign up (signup: open | invite),
|
|
411
|
+
which paths need sign-in (protected:), email, roles → permissions. Secrets stay in .env.local.
|
|
412
|
+
Add users later: npx ${PKG} add-user Organizations: npx ${PKG} add-org <name>
|
|
413
|
+
Working with an AI agent in this app? Point it at node_modules/${PKG}/AGENTS.md
|
|
240
414
|
`);
|
|
241
415
|
}
|
|
242
416
|
// ── main ──────────────────────────────────────────────────────
|
|
@@ -246,13 +420,58 @@ async function main() {
|
|
|
246
420
|
if (cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
247
421
|
console.log(` npx ${PKG} set up auth in this project (safe to re-run)
|
|
248
422
|
npx ${PKG} add-user [email] create a user
|
|
249
|
-
npx ${PKG} set-password [email]
|
|
423
|
+
npx ${PKG} set-password [email] also signs them out everywhere
|
|
424
|
+
npx ${PKG} remove-user [email] delete; also signs them out everywhere
|
|
425
|
+
npx ${PKG} sign-out-everywhere [email] end every session of a user
|
|
250
426
|
npx ${PKG} list-users
|
|
427
|
+
|
|
428
|
+
npx ${PKG} add-org <name> create an organization
|
|
429
|
+
npx ${PKG} add-member <email> <org> [role] add a user to an org (role: member, admin, owner, …)
|
|
430
|
+
npx ${PKG} remove-member <email> <org>
|
|
431
|
+
npx ${PKG} list-orgs
|
|
251
432
|
`);
|
|
252
433
|
return;
|
|
253
434
|
}
|
|
254
435
|
stepProject();
|
|
255
|
-
|
|
436
|
+
const USER_CMDS = ["add-user", "set-password", "remove-user", "sign-out-everywhere", "list-users"];
|
|
437
|
+
const ORG_CMDS = ["add-org", "add-member", "remove-member", "list-orgs"];
|
|
438
|
+
if (ORG_CMDS.includes(cmd)) {
|
|
439
|
+
await stepEnv();
|
|
440
|
+
const d = await db();
|
|
441
|
+
const [, email, orgName, role] = process.argv.slice(2);
|
|
442
|
+
const org = async (name) => {
|
|
443
|
+
if (!name)
|
|
444
|
+
fail(`Usage: npx ${PKG} ${cmd} <email> <org>`);
|
|
445
|
+
return (await d.findOrg(name)) ?? fail(`No organization "${name}". Create it: npx ${PKG} add-org "${name}"`);
|
|
446
|
+
};
|
|
447
|
+
const user = async (e) => {
|
|
448
|
+
if (!e)
|
|
449
|
+
fail(`Usage: npx ${PKG} ${cmd} <email> <org>`);
|
|
450
|
+
return (await d.findUser(e)) ?? fail(`No user "${e}".`);
|
|
451
|
+
};
|
|
452
|
+
if (cmd === "add-org") {
|
|
453
|
+
if (!arg)
|
|
454
|
+
fail(`Usage: npx ${PKG} add-org <name>`);
|
|
455
|
+
const o = await d.createOrg(arg);
|
|
456
|
+
done(`organization "${o.name}" created ${c.dim(o.id)}`);
|
|
457
|
+
}
|
|
458
|
+
if (cmd === "list-orgs") {
|
|
459
|
+
for (const o of await d.listOrgs())
|
|
460
|
+
console.log(` ${o.name} ${c.dim(o.id)} ${o.members} member${o.members === 1 ? "" : "s"}`);
|
|
461
|
+
}
|
|
462
|
+
if (cmd === "add-member") {
|
|
463
|
+
const [u, o] = [await user(email), await org(orgName)];
|
|
464
|
+
await d.addMember(o.id, u.id, role || "member");
|
|
465
|
+
done(`"${u.email}" is ${role || "member"} of "${o.name}"`);
|
|
466
|
+
}
|
|
467
|
+
if (cmd === "remove-member") {
|
|
468
|
+
const [u, o] = [await user(email), await org(orgName)];
|
|
469
|
+
(await d.removeMember(o.id, u.id)) ? done(`"${u.email}" removed from "${o.name}"`) : fail(`"${u.email}" is not a member of "${o.name}".`);
|
|
470
|
+
}
|
|
471
|
+
console.log();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (USER_CMDS.includes(cmd)) {
|
|
256
475
|
await stepEnv();
|
|
257
476
|
const d = await db();
|
|
258
477
|
if (cmd === "add-user")
|
|
@@ -268,7 +487,20 @@ async function main() {
|
|
|
268
487
|
fail("Password must be at least 8 characters.");
|
|
269
488
|
const { hashPassword } = await import("./core/password.js");
|
|
270
489
|
const ok = await d.setPassword(email, await hashPassword(password));
|
|
271
|
-
ok ? done(`password updated for "${email}"`) : fail(`No user "${email}".`);
|
|
490
|
+
ok ? done(`password updated for "${email}", old sessions ended`) : fail(`No user "${email}".`);
|
|
491
|
+
}
|
|
492
|
+
if (cmd === "remove-user" || cmd === "sign-out-everywhere") {
|
|
493
|
+
const email = (arg ?? (await ask("Email"))).trim().toLowerCase();
|
|
494
|
+
if (cmd === "remove-user") {
|
|
495
|
+
const sure = await ask(`Delete "${email}"? This cannot be undone. Type the email again`);
|
|
496
|
+
if (sure.trim().toLowerCase() !== email)
|
|
497
|
+
fail("Not deleted.");
|
|
498
|
+
(await d.removeUser(email)) ? done(`user "${email}" deleted, sessions ended`) : fail(`No user "${email}".`);
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
;
|
|
502
|
+
(await d.revokeSessions(email)) ? done(`every session of "${email}" ended`) : fail(`No user "${email}".`);
|
|
503
|
+
}
|
|
272
504
|
}
|
|
273
505
|
console.log();
|
|
274
506
|
return;
|
|
@@ -277,8 +509,9 @@ async function main() {
|
|
|
277
509
|
fail(`Unknown command "${cmd}". Try: npx ${PKG} help`);
|
|
278
510
|
const { appDir, src } = stepProject();
|
|
279
511
|
stepInstall();
|
|
280
|
-
await stepEnv();
|
|
512
|
+
await stepEnv({ askEmail: true });
|
|
281
513
|
stepFiles(appDir, src);
|
|
514
|
+
await stepConfig();
|
|
282
515
|
await stepSchema();
|
|
283
516
|
await stepFirstUser();
|
|
284
517
|
finish();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AdminOrg, AdminUser } from "./db.ts";
|
|
2
|
+
import type { Options } from "./options.ts";
|
|
3
|
+
export type AdminView = {
|
|
4
|
+
tab: "orgs" | "users" | "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
|
+
options: Options;
|
|
16
|
+
transport: string;
|
|
17
|
+
tokens: {
|
|
18
|
+
total: number;
|
|
19
|
+
live: number;
|
|
20
|
+
};
|
|
21
|
+
notice?: string;
|
|
22
|
+
error?: string;
|
|
23
|
+
/** per-render, for the one script the page carries; must match the CSP header */
|
|
24
|
+
nonce: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function adminPage(d: AdminPageData): string;
|