@pramen/auth 0.0.20 → 0.0.22
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/dist/index.js +28 -0
- package/package.json +2 -2
- package/src/index.ts +46 -0
package/dist/index.js
CHANGED
|
@@ -250,6 +250,34 @@ export function createMagicLinkAuth(opts) {
|
|
|
250
250
|
const sessionTtl = opts.sessionTtlSeconds ?? TOKEN_TTL_SECONDS;
|
|
251
251
|
const defaultRoles = opts.defaultRoles ?? DEFAULT_ROLES;
|
|
252
252
|
const handlers = {
|
|
253
|
+
/** Admin-only: create a passwordless user with the given roles (defaults if omitted)
|
|
254
|
+
* and email them a fresh magic link. Idempotent — inviting an existing user just
|
|
255
|
+
* resends the link and leaves their roles alone (admin uses setUserRoles for changes).
|
|
256
|
+
* Piggybacks on the sendMagicLinkEmail task, so the send happens outside the
|
|
257
|
+
* mutation's storage transaction. */
|
|
258
|
+
inviteUser: mutation(async (ctx, input) => {
|
|
259
|
+
const { email } = parseEmail(input);
|
|
260
|
+
const roles = Array.isArray(input?.roles) && input.roles.every((r) => typeof r === "string" && r.length > 0)
|
|
261
|
+
? input.roles
|
|
262
|
+
: defaultRoles;
|
|
263
|
+
const now = Date.now();
|
|
264
|
+
// Existence check on username (== email, per magic-link convention); a re-invite
|
|
265
|
+
// MUST NOT overwrite roles — that's setUserRoles's job.
|
|
266
|
+
const existing = await ctx.db.exec("SELECT username FROM auth_users WHERE username = ? LIMIT 1", email);
|
|
267
|
+
if (existing.length === 0) {
|
|
268
|
+
// Same shape as loginWithMagicLink's find-or-create path: username-only, no
|
|
269
|
+
// `email` column set (it stays a pure contact attribute, set via changeEmail).
|
|
270
|
+
await ctx.db.exec("INSERT INTO auth_users (username, passwordHash, roles, active, createdAt) VALUES (?, ?, ?, ?, ?)", email, "", JSON.stringify(roles), 1, now);
|
|
271
|
+
}
|
|
272
|
+
// Reuse magic-link machinery so login flow is identical whether the user was
|
|
273
|
+
// self-signed-up or admin-invited.
|
|
274
|
+
const token = mintToken();
|
|
275
|
+
const tokenHash = await sha256Hex(token);
|
|
276
|
+
await ctx.db.exec("DELETE FROM auth_magic_links WHERE email = ?", email);
|
|
277
|
+
await ctx.db.exec("INSERT INTO auth_magic_links (tokenHash, email, expiresAt, createdAt) VALUES (?, ?, ?, ?)", tokenHash, email, now + linkTtlMs, now);
|
|
278
|
+
await ctx.tasks.enqueue({ kind: "sendMagicLinkEmail", payload: { email, token } });
|
|
279
|
+
return { ok: true, created: existing.length === 0 };
|
|
280
|
+
}, { auth: ["admin"] }),
|
|
253
281
|
requestMagicLink: mutation(async (ctx, input) => {
|
|
254
282
|
const token = mintToken();
|
|
255
283
|
const tokenHash = await sha256Hex(token);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pramen/auth",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"description": "Optional credential→JWT login for pramen — signup/login/me + PBKDF2 hashing, issuing HS256 tokens the pramen verifier accepts.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -34,6 +34,6 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@pramen/server": "0.0.
|
|
37
|
+
"@pramen/server": "0.0.22"
|
|
38
38
|
}
|
|
39
39
|
}
|
package/src/index.ts
CHANGED
|
@@ -315,6 +315,52 @@ export function createMagicLinkAuth(opts: MagicLinkOptions): { handlers: Handler
|
|
|
315
315
|
const defaultRoles = opts.defaultRoles ?? DEFAULT_ROLES;
|
|
316
316
|
|
|
317
317
|
const handlers: HandlerMap = {
|
|
318
|
+
/** Admin-only: create a passwordless user with the given roles (defaults if omitted)
|
|
319
|
+
* and email them a fresh magic link. Idempotent — inviting an existing user just
|
|
320
|
+
* resends the link and leaves their roles alone (admin uses setUserRoles for changes).
|
|
321
|
+
* Piggybacks on the sendMagicLinkEmail task, so the send happens outside the
|
|
322
|
+
* mutation's storage transaction. */
|
|
323
|
+
inviteUser: mutation(
|
|
324
|
+
async (ctx, input: { email: string; roles?: string[] }) => {
|
|
325
|
+
const { email } = parseEmail(input);
|
|
326
|
+
const roles =
|
|
327
|
+
Array.isArray(input?.roles) && input.roles.every((r) => typeof r === "string" && r.length > 0)
|
|
328
|
+
? input.roles
|
|
329
|
+
: defaultRoles;
|
|
330
|
+
const now = Date.now();
|
|
331
|
+
// Existence check on username (== email, per magic-link convention); a re-invite
|
|
332
|
+
// MUST NOT overwrite roles — that's setUserRoles's job.
|
|
333
|
+
const existing = await ctx.db.exec("SELECT username FROM auth_users WHERE username = ? LIMIT 1", email);
|
|
334
|
+
if (existing.length === 0) {
|
|
335
|
+
// Same shape as loginWithMagicLink's find-or-create path: username-only, no
|
|
336
|
+
// `email` column set (it stays a pure contact attribute, set via changeEmail).
|
|
337
|
+
await ctx.db.exec(
|
|
338
|
+
"INSERT INTO auth_users (username, passwordHash, roles, active, createdAt) VALUES (?, ?, ?, ?, ?)",
|
|
339
|
+
email,
|
|
340
|
+
"",
|
|
341
|
+
JSON.stringify(roles),
|
|
342
|
+
1,
|
|
343
|
+
now,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
// Reuse magic-link machinery so login flow is identical whether the user was
|
|
347
|
+
// self-signed-up or admin-invited.
|
|
348
|
+
const token = mintToken();
|
|
349
|
+
const tokenHash = await sha256Hex(token);
|
|
350
|
+
await ctx.db.exec("DELETE FROM auth_magic_links WHERE email = ?", email);
|
|
351
|
+
await ctx.db.exec(
|
|
352
|
+
"INSERT INTO auth_magic_links (tokenHash, email, expiresAt, createdAt) VALUES (?, ?, ?, ?)",
|
|
353
|
+
tokenHash,
|
|
354
|
+
email,
|
|
355
|
+
now + linkTtlMs,
|
|
356
|
+
now,
|
|
357
|
+
);
|
|
358
|
+
await ctx.tasks.enqueue({ kind: "sendMagicLinkEmail", payload: { email, token } });
|
|
359
|
+
return { ok: true, created: existing.length === 0 };
|
|
360
|
+
},
|
|
361
|
+
{ auth: ["admin"] },
|
|
362
|
+
),
|
|
363
|
+
|
|
318
364
|
requestMagicLink: mutation(
|
|
319
365
|
async (ctx, input: { email: string }) => {
|
|
320
366
|
const token = mintToken();
|