@tapflowio/relay 0.25.0 → 0.26.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/dist/RelayServer.d.ts +15 -3
- package/dist/RelayServer.d.ts.map +1 -1
- package/dist/RelayServer.js +45 -15
- package/dist/RelayServer.js.map +1 -1
- package/dist/api/auth.d.ts +2 -1
- package/dist/api/auth.d.ts.map +1 -1
- package/dist/api/auth.js +28 -8
- package/dist/api/auth.js.map +1 -1
- package/dist/api/invitations.d.ts +3 -1
- package/dist/api/invitations.d.ts.map +1 -1
- package/dist/api/invitations.js +16 -20
- package/dist/api/invitations.js.map +1 -1
- package/dist/api/passwordReset.d.ts +14 -2
- package/dist/api/passwordReset.d.ts.map +1 -1
- package/dist/api/passwordReset.js +45 -10
- package/dist/api/passwordReset.js.map +1 -1
- package/dist/api/team.d.ts +3 -2
- package/dist/api/team.d.ts.map +1 -1
- package/dist/api/team.js +11 -5
- package/dist/api/team.js.map +1 -1
- package/dist/api/tokens.d.ts +2 -1
- package/dist/api/tokens.d.ts.map +1 -1
- package/dist/api/tokens.js +8 -2
- package/dist/api/tokens.js.map +1 -1
- package/dist/lib/adminAccount.d.ts +0 -5
- package/dist/lib/adminAccount.d.ts.map +1 -1
- package/dist/lib/adminAccount.js +6 -7
- package/dist/lib/adminAccount.js.map +1 -1
- package/dist/lib/connectionAuth.d.ts +35 -5
- package/dist/lib/connectionAuth.d.ts.map +1 -1
- package/dist/lib/connectionAuth.js +19 -3
- package/dist/lib/connectionAuth.js.map +1 -1
- package/dist/lib/email.d.ts +16 -0
- package/dist/lib/email.d.ts.map +1 -0
- package/dist/lib/email.js +18 -0
- package/dist/lib/email.js.map +1 -0
- package/dist/middleware/auth.d.ts +20 -1
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +33 -6
- package/dist/middleware/auth.js.map +1 -1
- package/dist/migrations/014_normalize_emails.sql +16 -0
- package/package.json +3 -3
- package/public/assets/{AppCenter-D_1rFI40.js → AppCenter-DZCsC2wa.js} +1 -1
- package/public/assets/AppCenter-DZCsC2wa.js.br +0 -0
- package/public/assets/AppCenter-DZCsC2wa.js.gz +0 -0
- package/public/assets/{Default-DF_X2_3I.js → Default-xD9Fmd5X.js} +1 -1
- package/public/assets/Default-xD9Fmd5X.js.br +0 -0
- package/public/assets/Default-xD9Fmd5X.js.gz +0 -0
- package/public/assets/Invite-29Erwv8j.js +1 -0
- package/public/assets/Invite-29Erwv8j.js.br +0 -0
- package/public/assets/Invite-29Erwv8j.js.gz +0 -0
- package/public/assets/{MacResources--1Enisxs.js → MacResources-B0DQ9ISE.js} +1 -1
- package/public/assets/MacResources-B0DQ9ISE.js.br +0 -0
- package/public/assets/MacResources-B0DQ9ISE.js.gz +0 -0
- package/public/assets/QASession-D-n6PXuD.js +150 -0
- package/public/assets/QASession-D-n6PXuD.js.br +0 -0
- package/public/assets/QASession-D-n6PXuD.js.gz +0 -0
- package/public/assets/{ResetPassword-B9miyXTP.js → ResetPassword-BnPIwTYH.js} +1 -1
- package/public/assets/ResetPassword-BnPIwTYH.js.br +0 -0
- package/public/assets/ResetPassword-BnPIwTYH.js.gz +0 -0
- package/public/assets/{Setup-CywZCqHU.js → Setup-DmUGFblg.js} +1 -1
- package/public/assets/Setup-DmUGFblg.js.br +0 -0
- package/public/assets/Setup-DmUGFblg.js.gz +0 -0
- package/public/assets/Team-BpvRgFfZ.js +6 -0
- package/public/assets/Team-BpvRgFfZ.js.br +0 -0
- package/public/assets/Team-BpvRgFfZ.js.gz +0 -0
- package/public/assets/Tokens-x6BtCcL7.js +1 -0
- package/public/assets/Tokens-x6BtCcL7.js.br +0 -0
- package/public/assets/Tokens-x6BtCcL7.js.gz +0 -0
- package/public/assets/{alert-dialog-CuPdBkxF.js → alert-dialog-C8wDimW-.js} +1 -1
- package/public/assets/alert-dialog-C8wDimW-.js.br +0 -0
- package/public/assets/alert-dialog-C8wDimW-.js.gz +0 -0
- package/public/assets/{badge-CSVsKOK_.js → badge-CWVbeuH3.js} +1 -1
- package/public/assets/badge-CWVbeuH3.js.br +0 -0
- package/public/assets/badge-CWVbeuH3.js.gz +0 -0
- package/public/assets/{build-format-l9BrlbZl.js → build-format-fzt-JrUX.js} +1 -1
- package/public/assets/build-format-fzt-JrUX.js.br +0 -0
- package/public/assets/build-format-fzt-JrUX.js.gz +0 -0
- package/public/assets/{dialog-B6jt7-2f.js → dialog-DQkbaw8h.js} +2 -2
- package/public/assets/dialog-DQkbaw8h.js.br +0 -0
- package/public/assets/dialog-DQkbaw8h.js.gz +0 -0
- package/public/assets/{index-Cm17I31a.js → index-C1Fo8g_c.js} +32 -32
- package/public/assets/index-C1Fo8g_c.js.br +0 -0
- package/public/assets/index-C1Fo8g_c.js.gz +0 -0
- package/public/assets/index-C2F1AgwB.css +1 -0
- package/public/assets/index-C2F1AgwB.css.br +0 -0
- package/public/assets/{index-D_P99bPX.css.gz → index-C2F1AgwB.css.gz} +0 -0
- package/public/assets/{list-view-Bp6pvsde.js → list-view-DxI397-H.js} +1 -1
- package/public/assets/list-view-DxI397-H.js.br +0 -0
- package/public/assets/list-view-DxI397-H.js.gz +0 -0
- package/public/assets/{pencil-g-WiK1F2.js → pencil-BJcOp3oN.js} +1 -1
- package/public/assets/pencil-BJcOp3oN.js.br +0 -0
- package/public/assets/pencil-BJcOp3oN.js.gz +0 -0
- package/public/assets/{resource-health-BbeyA8an.js → resource-health-Cq3gqqVd.js} +1 -1
- package/public/assets/resource-health-Cq3gqqVd.js.br +0 -0
- package/public/assets/resource-health-Cq3gqqVd.js.gz +0 -0
- package/public/assets/{trash-DHPg15P4.js → trash-DhBxh6H7.js} +1 -1
- package/public/assets/trash-DhBxh6H7.js.br +0 -0
- package/public/assets/trash-DhBxh6H7.js.gz +0 -0
- package/public/index.html +2 -2
- package/public/index.html.br +0 -0
- package/public/index.html.gz +0 -0
- package/public/assets/AppCenter-D_1rFI40.js.br +0 -0
- package/public/assets/AppCenter-D_1rFI40.js.gz +0 -0
- package/public/assets/Default-DF_X2_3I.js.br +0 -0
- package/public/assets/Default-DF_X2_3I.js.gz +0 -0
- package/public/assets/Invite-DETTWdYp.js +0 -1
- package/public/assets/Invite-DETTWdYp.js.br +0 -0
- package/public/assets/Invite-DETTWdYp.js.gz +0 -0
- package/public/assets/MacResources--1Enisxs.js.br +0 -0
- package/public/assets/MacResources--1Enisxs.js.gz +0 -0
- package/public/assets/QASession-BjoD9Toj.js +0 -150
- package/public/assets/QASession-BjoD9Toj.js.br +0 -0
- package/public/assets/QASession-BjoD9Toj.js.gz +0 -0
- package/public/assets/ResetPassword-B9miyXTP.js.br +0 -0
- package/public/assets/ResetPassword-B9miyXTP.js.gz +0 -0
- package/public/assets/Setup-CywZCqHU.js.br +0 -0
- package/public/assets/Setup-CywZCqHU.js.gz +0 -0
- package/public/assets/Team-CxdX_9mY.js +0 -6
- package/public/assets/Team-CxdX_9mY.js.br +0 -0
- package/public/assets/Team-CxdX_9mY.js.gz +0 -0
- package/public/assets/Tokens-BQnnawvG.js +0 -1
- package/public/assets/Tokens-BQnnawvG.js.br +0 -0
- package/public/assets/Tokens-BQnnawvG.js.gz +0 -0
- package/public/assets/alert-dialog-CuPdBkxF.js.br +0 -0
- package/public/assets/alert-dialog-CuPdBkxF.js.gz +0 -0
- package/public/assets/badge-CSVsKOK_.js.br +0 -0
- package/public/assets/badge-CSVsKOK_.js.gz +0 -0
- package/public/assets/build-format-l9BrlbZl.js.br +0 -0
- package/public/assets/build-format-l9BrlbZl.js.gz +0 -0
- package/public/assets/dialog-B6jt7-2f.js.br +0 -0
- package/public/assets/dialog-B6jt7-2f.js.gz +0 -0
- package/public/assets/index-Cm17I31a.js.br +0 -0
- package/public/assets/index-Cm17I31a.js.gz +0 -0
- package/public/assets/index-D_P99bPX.css +0 -1
- package/public/assets/index-D_P99bPX.css.br +0 -0
- package/public/assets/list-view-Bp6pvsde.js.br +0 -0
- package/public/assets/list-view-Bp6pvsde.js.gz +0 -0
- package/public/assets/pencil-g-WiK1F2.js.br +0 -0
- package/public/assets/pencil-g-WiK1F2.js.gz +0 -0
- package/public/assets/resource-health-BbeyA8an.js.br +0 -0
- package/public/assets/resource-health-BbeyA8an.js.gz +0 -0
- package/public/assets/trash-DHPg15P4.js.br +0 -0
- package/public/assets/trash-DHPg15P4.js.gz +0 -0
package/dist/api/auth.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getDb } from '../db.js';
|
|
2
2
|
import { makePasswordHash, verifyPassword, isInitialized, createAdminAccount } from '../lib/adminAccount.js';
|
|
3
|
-
import {
|
|
3
|
+
import { sessionCookie, requireAuth } from '../middleware/auth.js';
|
|
4
|
+
import { normalizeEmail, EMAIL_KEY_SQL } from '../lib/email.js';
|
|
4
5
|
import { json, readJson } from '../router.js';
|
|
5
6
|
import { config } from '../lib/config.js';
|
|
6
7
|
import { resolveRequestClient } from '../lib/clientAddress.js';
|
|
@@ -15,25 +16,40 @@ export async function handleLogin(req, res, trustedProxies = config.local.truste
|
|
|
15
16
|
const body = await readJson(req);
|
|
16
17
|
if (!body.email || !body.password)
|
|
17
18
|
return json(res, 400, { error: 'email and password required' });
|
|
18
|
-
const key = `${resolveRequestClient(req, trustedProxies).addr}|${body.email
|
|
19
|
+
const key = `${resolveRequestClient(req, trustedProxies).addr}|${normalizeEmail(body.email)}`;
|
|
19
20
|
const gate = limiter.check(key);
|
|
20
21
|
if (!gate.allowed) {
|
|
21
22
|
res.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': String(Math.ceil(gate.retryAfterMs / 1000)) });
|
|
22
23
|
res.end(JSON.stringify({ error: 'Too many attempts. Try again later.' }));
|
|
23
24
|
return;
|
|
24
25
|
}
|
|
25
|
-
const
|
|
26
|
-
const user = db.prepare('SELECT id, email, role, password_hash FROM users WHERE email = ?').get(body.email);
|
|
26
|
+
const user = findLoginUser(body.email);
|
|
27
27
|
if (!user || !user.password_hash || !verifyPassword(body.password, user.password_hash)) {
|
|
28
28
|
limiter.recordFailure(key);
|
|
29
29
|
return json(res, 401, { error: 'Invalid credentials' });
|
|
30
30
|
}
|
|
31
31
|
limiter.reset(key);
|
|
32
|
-
const
|
|
33
|
-
const cookie = `tapflow_token=${token}; HttpOnly; Path=/; SameSite=Lax; Max-Age=${7 * 24 * 3600}`;
|
|
32
|
+
const cookie = sessionCookie({ userId: user.id, email: user.email, role: user.role });
|
|
34
33
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Set-Cookie': cookie });
|
|
35
34
|
res.end(JSON.stringify({ ok: true, role: user.role }));
|
|
36
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* The account an address signs in to. The address exactly as typed comes first, so each row of a pair
|
|
38
|
+
* migration 014 had to leave alone (two addresses that normalize to the same one) still signs in by its
|
|
39
|
+
* own address — tried second, `alice@x.com` would shadow `Alice@x.com` for good. Then the normalized
|
|
40
|
+
* form, which migration 014 and every write since store; then a normalized comparison that must be
|
|
41
|
+
* unambiguous, so an address matching both rows of a pair signs in to neither.
|
|
42
|
+
*/
|
|
43
|
+
function findLoginUser(email) {
|
|
44
|
+
const db = getDb();
|
|
45
|
+
const byExact = db.prepare('SELECT id, email, role, password_hash FROM users WHERE email = ?');
|
|
46
|
+
const found = byExact.get(email) ?? byExact.get(normalizeEmail(email));
|
|
47
|
+
if (found)
|
|
48
|
+
return found;
|
|
49
|
+
const byKey = db.prepare(`SELECT id, email, role, password_hash FROM users WHERE ${EMAIL_KEY_SQL} = ?`)
|
|
50
|
+
.all(normalizeEmail(email));
|
|
51
|
+
return byKey.length === 1 ? byKey[0] : undefined;
|
|
52
|
+
}
|
|
37
53
|
export function handleMe(req, res) {
|
|
38
54
|
const auth = requireAuth(req, res);
|
|
39
55
|
if (!auth)
|
|
@@ -45,7 +61,7 @@ export function handleMe(req, res) {
|
|
|
45
61
|
const displayName = user.display_name ?? user.email.split('@')[0];
|
|
46
62
|
json(res, 200, { id: user.id, email: user.email, displayName, avatarUrl: user.avatar_url, role: user.role });
|
|
47
63
|
}
|
|
48
|
-
export async function handleChangePassword(req, res) {
|
|
64
|
+
export async function handleChangePassword(req, res, onAuthChanged = () => { }) {
|
|
49
65
|
const auth = requireAuth(req, res);
|
|
50
66
|
if (!auth)
|
|
51
67
|
return;
|
|
@@ -61,7 +77,11 @@ export async function handleChangePassword(req, res) {
|
|
|
61
77
|
if (!verifyPassword(body.currentPassword, user.password_hash))
|
|
62
78
|
return json(res, 401, { error: 'Incorrect current password' });
|
|
63
79
|
db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(makePasswordHash(body.newPassword), auth.userId);
|
|
64
|
-
|
|
80
|
+
// The new hash ends every session issued under the old one, this browser's included — so this browser
|
|
81
|
+
// gets a fresh cookie, and the others (a stolen one among them) are signed out. Open sockets follow.
|
|
82
|
+
onAuthChanged({ userId: auth.userId, scope: 'sessions' });
|
|
83
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Set-Cookie': sessionCookie(auth) });
|
|
84
|
+
res.end(JSON.stringify({ ok: true }));
|
|
65
85
|
}
|
|
66
86
|
export function handleLogout(_req, res) {
|
|
67
87
|
res.writeHead(200, {
|
package/dist/api/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/api/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAChC,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC5G,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/api/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
export declare function handleVerify(req: http.IncomingMessage, res: http.ServerResponse): void;
|
|
3
|
-
export declare function handleAccept(req: http.IncomingMessage, res: http.ServerResponse, uploadsDir: string
|
|
3
|
+
export declare function handleAccept(req: http.IncomingMessage, res: http.ServerResponse, uploadsDir: string): void;
|
|
4
|
+
/** Whether an address already belongs to a member, however it was capitalized or spaced when stored. */
|
|
5
|
+
export declare function isMemberEmail(email: string): boolean;
|
|
4
6
|
//# sourceMappingURL=invitations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"invitations.d.ts","sourceRoot":"","sources":["../../src/api/invitations.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"invitations.d.ts","sourceRoot":"","sources":["../../src/api/invitations.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAWvB,wBAAgB,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,IAAI,CAYtF;AAED,wBAAgB,YAAY,CAC1B,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,UAAU,EAAE,MAAM,GACjB,IAAI,CAwEN;AAED,wGAAwG;AACxG,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEpD"}
|
package/dist/api/invitations.js
CHANGED
|
@@ -3,7 +3,8 @@ import path from 'path';
|
|
|
3
3
|
import crypto from 'crypto';
|
|
4
4
|
import busboy from 'busboy';
|
|
5
5
|
import { getDb } from '../db.js';
|
|
6
|
-
import {
|
|
6
|
+
import { sessionCookie } from '../middleware/auth.js';
|
|
7
|
+
import { normalizeEmail, EMAIL_KEY_SQL, SYNTHETIC_EMAIL_DOMAIN } from '../lib/email.js';
|
|
7
8
|
import { makePasswordHash } from './auth.js';
|
|
8
9
|
import { json } from '../router.js';
|
|
9
10
|
export function handleVerify(req, res) {
|
|
@@ -19,7 +20,7 @@ export function handleVerify(req, res) {
|
|
|
19
20
|
return json(res, 410, { error: 'Invitation expired or not found' });
|
|
20
21
|
json(res, 200, { role: inv.role });
|
|
21
22
|
}
|
|
22
|
-
export function handleAccept(req, res, uploadsDir
|
|
23
|
+
export function handleAccept(req, res, uploadsDir) {
|
|
23
24
|
const bb = busboy({ headers: req.headers, limits: { fileSize: 2 * 1024 * 1024 } });
|
|
24
25
|
const fields = {};
|
|
25
26
|
let fileBuffer = null;
|
|
@@ -62,19 +63,15 @@ export function handleAccept(req, res, uploadsDir, onAuthChanged = () => { }) {
|
|
|
62
63
|
return json(res, 410, { error: 'Invitation expired or not found' });
|
|
63
64
|
const passwordHash = makePasswordHash(password);
|
|
64
65
|
const displayName = fields.display_name?.trim() || (inv.email?.split('@')[0] ?? 'User');
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
.run(passwordHash, inv.role, displayName, existing.id);
|
|
72
|
-
userId = existing.id;
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
const result = db.prepare('INSERT INTO users (email, role, password_hash, display_name) VALUES (?, ?, ?, ?)').run(inv.email ?? `user_${crypto.randomBytes(4).toString('hex')}@tapflow.local`, inv.role, passwordHash, displayName);
|
|
76
|
-
userId = result.lastInsertRowid;
|
|
66
|
+
// An invitation creates an account; it never changes one. Accepting for an address that already
|
|
67
|
+
// has an account used to overwrite that account's password, role and name — an invite link worked
|
|
68
|
+
// as a password reset nobody approved. The address may have joined since the invitation was made,
|
|
69
|
+
// so this is checked here as well as when inviting.
|
|
70
|
+
if (inv.email && isMemberEmail(inv.email)) {
|
|
71
|
+
return json(res, 409, { error: 'This email already has an account. Sign in instead.' });
|
|
77
72
|
}
|
|
73
|
+
const result = db.prepare('INSERT INTO users (email, role, password_hash, display_name) VALUES (?, ?, ?, ?)').run(inv.email ? normalizeEmail(inv.email) : `user_${crypto.randomBytes(4).toString('hex')}${SYNTHETIC_EMAIL_DOMAIN}`, inv.role, passwordHash, displayName);
|
|
74
|
+
const userId = result.lastInsertRowid;
|
|
78
75
|
if (fileBuffer && fileMime) {
|
|
79
76
|
const ext = fileMime === 'image/png' ? '.png' : '.jpg';
|
|
80
77
|
const avatarPath = path.join(uploadsDir, 'avatars', `user-${userId}${ext}`);
|
|
@@ -83,17 +80,16 @@ export function handleAccept(req, res, uploadsDir, onAuthChanged = () => { }) {
|
|
|
83
80
|
db.prepare('UPDATE users SET avatar_url = ? WHERE id = ?').run(`/uploads/avatars/user-${userId}${ext}`, userId);
|
|
84
81
|
}
|
|
85
82
|
db.prepare("UPDATE invitations SET used_at = datetime('now') WHERE id = ?").run(inv.id);
|
|
86
|
-
// Accepting for an email that already has an account overwrites its role, which can take an
|
|
87
|
-
// Admin's agent authority away from sockets already open.
|
|
88
|
-
if (existing)
|
|
89
|
-
onAuthChanged();
|
|
90
83
|
const user = db.prepare('SELECT email, role FROM users WHERE id = ?').get(userId);
|
|
91
|
-
const
|
|
92
|
-
const cookie = `tapflow_token=${jwtToken}; HttpOnly; Path=/; SameSite=Lax; Max-Age=${7 * 24 * 3600}`;
|
|
84
|
+
const cookie = sessionCookie({ userId, email: user.email, role: user.role });
|
|
93
85
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Set-Cookie': cookie });
|
|
94
86
|
res.end(JSON.stringify({ ok: true }));
|
|
95
87
|
});
|
|
96
88
|
bb.on('error', () => json(res, 500, { error: 'Failed to process request' }));
|
|
97
89
|
req.pipe(bb);
|
|
98
90
|
}
|
|
91
|
+
/** Whether an address already belongs to a member, however it was capitalized or spaced when stored. */
|
|
92
|
+
export function isMemberEmail(email) {
|
|
93
|
+
return getDb().prepare(`SELECT 1 FROM users WHERE ${EMAIL_KEY_SQL} = ?`).get(normalizeEmail(email)) !== undefined;
|
|
94
|
+
}
|
|
99
95
|
//# sourceMappingURL=invitations.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"invitations.js","sourceRoot":"","sources":["../../src/api/invitations.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,IAAI,MAAM,MAAM,CAAA;AACvB,OAAO,MAAM,MAAM,QAAQ,CAAA;AAC3B,OAAO,MAAM,MAAM,QAAQ,CAAA;AAC3B,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAChC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"invitations.js","sourceRoot":"","sources":["../../src/api/invitations.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
|
+
import type { AuthChange } from '../lib/connectionAuth.js';
|
|
2
3
|
import { type TapflowConfig } from '../lib/config.js';
|
|
3
4
|
import { type TunnelRuntime } from '../lib/publicUrl.js';
|
|
4
5
|
export declare function buildPasswordResetUrl(token: string, cfg: Pick<TapflowConfig, 'tunnel' | 'relay' | 'local'>, tunnel?: TunnelRuntime): string;
|
|
5
6
|
export declare function passwordResetLinkWarning(url: string): string | null;
|
|
6
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Issues a reset link for a member and mails it when mail can reach them. Returns the token either way:
|
|
9
|
+
* the Admin who asked also gets the link, as with an invitation, so an install without SMTP can still
|
|
10
|
+
* reset a password.
|
|
11
|
+
*
|
|
12
|
+
* Only the newest link works. The earlier unused ones are spent here, so a link sent to the wrong place
|
|
13
|
+
* is dead as soon as the Admin makes another.
|
|
14
|
+
*/
|
|
15
|
+
export declare function issuePasswordReset(userId: number, tunnel?: TunnelRuntime): Promise<{
|
|
16
|
+
token: string;
|
|
17
|
+
emailSent: boolean;
|
|
18
|
+
} | null>;
|
|
7
19
|
export declare function handleVerifyReset(req: http.IncomingMessage, res: http.ServerResponse): void;
|
|
8
|
-
export declare function handleDoReset(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;
|
|
20
|
+
export declare function handleDoReset(req: http.IncomingMessage, res: http.ServerResponse, onAuthChanged?: (affected?: AuthChange) => void): Promise<void>;
|
|
9
21
|
export declare function handleSendMemberReset(req: http.IncomingMessage, res: http.ServerResponse, params: Record<string, string>, tunnel?: TunnelRuntime): Promise<void>;
|
|
10
22
|
//# sourceMappingURL=passwordReset.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"passwordReset.d.ts","sourceRoot":"","sources":["../../src/api/passwordReset.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAQvB,OAAO,EAAU,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAC7D,OAAO,
|
|
1
|
+
{"version":3,"file":"passwordReset.d.ts","sourceRoot":"","sources":["../../src/api/passwordReset.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAQvB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AAC1D,OAAO,EAAU,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAC7D,OAAO,EAA0D,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAQhH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC,EACtD,MAAM,CAAC,EAAE,aAAa,GACrB,MAAM,CAER;AAED,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEnE;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAAC,CA4BvD;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,IAAI,CAY3F;AAED,wBAAsB,aAAa,CACjC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,aAAa,GAAE,CAAC,QAAQ,CAAC,EAAE,UAAU,KAAK,IAAe,GACxD,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC,IAAI,CAAC,CAiBf"}
|
|
@@ -6,7 +6,8 @@ import { makePasswordHash } from './auth.js';
|
|
|
6
6
|
import { sendMail } from '../lib/mailer.js';
|
|
7
7
|
import { json, readJson } from '../router.js';
|
|
8
8
|
import { config } from '../lib/config.js';
|
|
9
|
-
import { buildInviteBaseUrl } from '../lib/publicUrl.js';
|
|
9
|
+
import { buildInviteBaseUrl, forTeammates, resolvePublicBaseUrl } from '../lib/publicUrl.js';
|
|
10
|
+
import { SYNTHETIC_EMAIL_DOMAIN } from '../lib/email.js';
|
|
10
11
|
const logger = createLogger('relay:password-reset');
|
|
11
12
|
const INSECURE_RESET_LINK_WARNING = 'Password-reset email uses insecure HTTP. Reset tokens may be exposed in transit; configure HTTPS with tunnel.publicUrl or relay.url.';
|
|
12
13
|
let warnedInsecureResetLink = false;
|
|
@@ -16,14 +17,29 @@ export function buildPasswordResetUrl(token, cfg, tunnel) {
|
|
|
16
17
|
export function passwordResetLinkWarning(url) {
|
|
17
18
|
return /^https:\/\//i.test(url) ? null : INSECURE_RESET_LINK_WARNING;
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Issues a reset link for a member and mails it when mail can reach them. Returns the token either way:
|
|
22
|
+
* the Admin who asked also gets the link, as with an invitation, so an install without SMTP can still
|
|
23
|
+
* reset a password.
|
|
24
|
+
*
|
|
25
|
+
* Only the newest link works. The earlier unused ones are spent here, so a link sent to the wrong place
|
|
26
|
+
* is dead as soon as the Admin makes another.
|
|
27
|
+
*/
|
|
28
|
+
export async function issuePasswordReset(userId, tunnel) {
|
|
20
29
|
const db = getDb();
|
|
21
30
|
const user = db.prepare('SELECT id, email FROM users WHERE id = ?').get(userId);
|
|
22
31
|
if (!user)
|
|
23
|
-
return
|
|
32
|
+
return null;
|
|
24
33
|
const token = crypto.randomBytes(32).toString('hex');
|
|
25
34
|
const expiresAt = new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString();
|
|
26
|
-
db.
|
|
35
|
+
db.transaction(() => {
|
|
36
|
+
db.prepare("UPDATE password_reset_tokens SET used_at = datetime('now') WHERE user_id = ? AND used_at IS NULL").run(userId);
|
|
37
|
+
db.prepare('INSERT INTO password_reset_tokens (user_id, token, expires_at) VALUES (?, ?, ?)').run(userId, token, expiresAt);
|
|
38
|
+
})();
|
|
39
|
+
// A member invited without an email has a made-up `@tapflow.local` address. An SMTP server may well
|
|
40
|
+
// accept mail for it, which would report "sent" for a message nobody can receive.
|
|
41
|
+
if (user.email.endsWith(SYNTHETIC_EMAIL_DOMAIN))
|
|
42
|
+
return { token, emailSent: false };
|
|
27
43
|
const link = buildPasswordResetUrl(token, config, tunnel);
|
|
28
44
|
const warning = passwordResetLinkWarning(link);
|
|
29
45
|
if (warning !== null && !warnedInsecureResetLink) {
|
|
@@ -33,7 +49,7 @@ export async function sendPasswordResetEmail(userId, tunnel) {
|
|
|
33
49
|
const html = `<p>A password reset was requested for your tapflow account.</p>
|
|
34
50
|
<p><a href="${link}">Reset your password</a></p>
|
|
35
51
|
<p>This link expires in 2 hours. If you did not request this, ignore this email.</p>`;
|
|
36
|
-
return sendMail(user.email, 'Reset your tapflow password', html);
|
|
52
|
+
return { token, emailSent: await sendMail(user.email, 'Reset your tapflow password', html) };
|
|
37
53
|
}
|
|
38
54
|
export function handleVerifyReset(req, res) {
|
|
39
55
|
const token = new URL(req.url ?? '/', 'http://x').searchParams.get('token');
|
|
@@ -48,7 +64,7 @@ export function handleVerifyReset(req, res) {
|
|
|
48
64
|
return json(res, 410, { error: 'Token expired or not found' });
|
|
49
65
|
json(res, 200, { ok: true });
|
|
50
66
|
}
|
|
51
|
-
export async function handleDoReset(req, res) {
|
|
67
|
+
export async function handleDoReset(req, res, onAuthChanged = () => { }) {
|
|
52
68
|
const body = await readJson(req);
|
|
53
69
|
if (!body.token || !body.password)
|
|
54
70
|
return json(res, 400, { error: 'token and password required' });
|
|
@@ -61,8 +77,20 @@ export async function handleDoReset(req, res) {
|
|
|
61
77
|
`).get(body.token);
|
|
62
78
|
if (!row)
|
|
63
79
|
return json(res, 410, { error: 'Token expired or not found' });
|
|
64
|
-
db.
|
|
65
|
-
|
|
80
|
+
db.transaction(() => {
|
|
81
|
+
db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(makePasswordHash(body.password), row.user_id);
|
|
82
|
+
// Every outstanding link for this user, not only the one used: the password they unlock is gone.
|
|
83
|
+
db.prepare("UPDATE password_reset_tokens SET used_at = datetime('now') WHERE user_id = ? AND used_at IS NULL").run(row.user_id);
|
|
84
|
+
// A reset is how an account is recovered, and whoever held it may have minted tokens that a new
|
|
85
|
+
// password alone would leave working. So a reset revokes all of them, CI and agent tokens included;
|
|
86
|
+
// the owner issues new ones. A self-service change does not: it needs the current password, and
|
|
87
|
+
// revoking there would break the owner's own CI without warning.
|
|
88
|
+
db.prepare('DELETE FROM personal_access_tokens WHERE user_id = ?').run(row.user_id);
|
|
89
|
+
})();
|
|
90
|
+
// The new hash has already ended the user's sessions (`passwordVersion`) and the tokens are gone;
|
|
91
|
+
// this closes their open sockets, token-authenticated agents included, now rather than on the next
|
|
92
|
+
// heartbeat.
|
|
93
|
+
onAuthChanged({ userId: row.user_id });
|
|
66
94
|
json(res, 200, { ok: true });
|
|
67
95
|
}
|
|
68
96
|
export async function handleSendMemberReset(req, res, params, tunnel) {
|
|
@@ -73,7 +101,14 @@ export async function handleSendMemberReset(req, res, params, tunnel) {
|
|
|
73
101
|
const member = db.prepare('SELECT id FROM users WHERE id = ?').get(params.id);
|
|
74
102
|
if (!member)
|
|
75
103
|
return json(res, 404, { error: 'Member not found' });
|
|
76
|
-
const
|
|
77
|
-
|
|
104
|
+
const issued = await issuePasswordReset(member.id, tunnel);
|
|
105
|
+
if (!issued)
|
|
106
|
+
return json(res, 404, { error: 'Member not found' });
|
|
107
|
+
// The same rule as an invitation's `inviteUrl` (#788): null when the only address is a loopback one,
|
|
108
|
+
// and the dashboard builds the link from the browser's origin with `token` instead.
|
|
109
|
+
const resetUrl = forTeammates(resolvePublicBaseUrl(config, tunnel)) === null
|
|
110
|
+
? null
|
|
111
|
+
: buildPasswordResetUrl(issued.token, config, tunnel);
|
|
112
|
+
json(res, 200, { ok: true, emailSent: issued.emailSent, token: issued.token, resetUrl });
|
|
78
113
|
}
|
|
79
114
|
//# sourceMappingURL=passwordReset.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"passwordReset.js","sourceRoot":"","sources":["../../src/api/passwordReset.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,QAAQ,CAAA;AAC3B,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAChC,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"passwordReset.js","sourceRoot":"","sources":["../../src/api/passwordReset.ts"],"names":[],"mappings":"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"}
|
package/dist/api/team.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
|
+
import type { AuthChange } from '../lib/connectionAuth.js';
|
|
2
3
|
import { type TunnelRuntime } from '../lib/publicUrl.js';
|
|
3
4
|
export declare function handleListMembers(req: http.IncomingMessage, res: http.ServerResponse): void;
|
|
4
5
|
export declare function handleInvite(req: http.IncomingMessage, res: http.ServerResponse, tunnel?: TunnelRuntime): Promise<void>;
|
|
5
|
-
export declare function handleUpdateMember(req: http.IncomingMessage, res: http.ServerResponse, params: Record<string, string>, onAuthChanged?: () => void): Promise<void>;
|
|
6
|
-
export declare function handleDeleteMember(req: http.IncomingMessage, res: http.ServerResponse, params: Record<string, string>, onAuthChanged?: () => void): void;
|
|
6
|
+
export declare function handleUpdateMember(req: http.IncomingMessage, res: http.ServerResponse, params: Record<string, string>, onAuthChanged?: (affected?: AuthChange) => void): Promise<void>;
|
|
7
|
+
export declare function handleDeleteMember(req: http.IncomingMessage, res: http.ServerResponse, params: Record<string, string>, onAuthChanged?: (affected?: AuthChange) => void): void;
|
|
7
8
|
//# sourceMappingURL=team.d.ts.map
|
package/dist/api/team.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"team.d.ts","sourceRoot":"","sources":["../../src/api/team.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"team.d.ts","sourceRoot":"","sources":["../../src/api/team.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAKvB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AAK1D,OAAO,EAA0D,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAEhH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,IAAI,CAS3F;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAiC7H;AAED,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,aAAa,GAAE,CAAC,QAAQ,CAAC,EAAE,UAAU,KAAK,IAAe,GACxD,OAAO,CAAC,IAAI,CAAC,CAef;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,aAAa,GAAE,CAAC,QAAQ,CAAC,EAAE,UAAU,KAAK,IAAe,GACxD,IAAI,CAcN"}
|
package/dist/api/team.js
CHANGED
|
@@ -4,6 +4,8 @@ import { requireRole } from '../middleware/auth.js';
|
|
|
4
4
|
import { json, readJson } from '../router.js';
|
|
5
5
|
import { sendMail } from '../lib/mailer.js';
|
|
6
6
|
import { config } from '../lib/config.js';
|
|
7
|
+
import { normalizeEmail } from '../lib/email.js';
|
|
8
|
+
import { isMemberEmail } from './invitations.js';
|
|
7
9
|
import { buildInviteBaseUrl, forTeammates, resolvePublicBaseUrl } from '../lib/publicUrl.js';
|
|
8
10
|
export function handleListMembers(req, res) {
|
|
9
11
|
const auth = requireRole(req, res, ['Admin']);
|
|
@@ -18,6 +20,10 @@ export async function handleInvite(req, res, tunnel) {
|
|
|
18
20
|
if (!auth)
|
|
19
21
|
return;
|
|
20
22
|
const body = await readJson(req);
|
|
23
|
+
const email = body.email ? normalizeEmail(body.email) : '';
|
|
24
|
+
// Inviting a member would only produce a link that is refused on accept.
|
|
25
|
+
if (email && isMemberEmail(email))
|
|
26
|
+
return json(res, 409, { error: 'Already a member' });
|
|
21
27
|
const role = ['Admin', 'Developer', 'QA', 'Viewer'].includes(body.role ?? '')
|
|
22
28
|
? body.role
|
|
23
29
|
: 'QA';
|
|
@@ -25,17 +31,17 @@ export async function handleInvite(req, res, tunnel) {
|
|
|
25
31
|
const expiresAt = new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString();
|
|
26
32
|
const db = getDb();
|
|
27
33
|
db.prepare('INSERT INTO invitations (token, email, role, expires_at) VALUES (?, ?, ?, ?)')
|
|
28
|
-
.run(token,
|
|
34
|
+
.run(token, email || null, role, expiresAt);
|
|
29
35
|
// The dashboard copies the same link the mail carries (#788). It gets null when the only address is
|
|
30
36
|
// the mail's localhost fallback or a loopback relay.url, and builds from the browser's origin instead.
|
|
31
37
|
const inviteUrl = `${buildInviteBaseUrl(config, tunnel)}/invite?token=${token}`;
|
|
32
38
|
const offered = forTeammates(resolvePublicBaseUrl(config, tunnel)) === null ? null : inviteUrl;
|
|
33
39
|
let emailSent = false;
|
|
34
|
-
if (
|
|
40
|
+
if (email) {
|
|
35
41
|
const html = `<p>You've been invited to join tapflow as <strong>${role}</strong>.</p>
|
|
36
42
|
<p><a href="${inviteUrl}">Accept invitation</a></p>
|
|
37
43
|
<p>This link expires in 7 days.</p>`;
|
|
38
|
-
emailSent = await sendMail(
|
|
44
|
+
emailSent = await sendMail(email, 'You have been invited to tapflow', html);
|
|
39
45
|
}
|
|
40
46
|
json(res, 201, { token, emailSent, inviteUrl: offered });
|
|
41
47
|
}
|
|
@@ -52,7 +58,7 @@ export async function handleUpdateMember(req, res, params, onAuthChanged = () =>
|
|
|
52
58
|
if (result.changes === 0)
|
|
53
59
|
return json(res, 404, { error: 'Member not found' });
|
|
54
60
|
// A demoted Admin's agent tokens stop working now, including on sockets already open.
|
|
55
|
-
onAuthChanged();
|
|
61
|
+
onAuthChanged({ userId: Number(params.id) });
|
|
56
62
|
json(res, 200, { ok: true });
|
|
57
63
|
}
|
|
58
64
|
export function handleDeleteMember(req, res, params, onAuthChanged = () => { }) {
|
|
@@ -67,7 +73,7 @@ export function handleDeleteMember(req, res, params, onAuthChanged = () => { })
|
|
|
67
73
|
if (result.changes === 0)
|
|
68
74
|
return json(res, 404, { error: 'Member not found' });
|
|
69
75
|
// Their cookie and PATs (cascaded) stop working on HTTP at once; this closes their open sockets too.
|
|
70
|
-
onAuthChanged();
|
|
76
|
+
onAuthChanged({ userId: Number(params.id) });
|
|
71
77
|
json(res, 204, null);
|
|
72
78
|
}
|
|
73
79
|
//# sourceMappingURL=team.js.map
|
package/dist/api/team.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"team.js","sourceRoot":"","sources":["../../src/api/team.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,QAAQ,CAAA;AAC3B,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAChC,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACnD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"team.js","sourceRoot":"","sources":["../../src/api/team.ts"],"names":[],"mappings":"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"}
|
package/dist/api/tokens.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
|
+
import { type AuthChange } from '../lib/connectionAuth.js';
|
|
2
3
|
export declare function handleListTokens(req: http.IncomingMessage, res: http.ServerResponse): void;
|
|
3
4
|
export declare function handleCreateToken(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;
|
|
4
|
-
export declare function handleRevokeToken(req: http.IncomingMessage, res: http.ServerResponse, params: Record<string, string>, onAuthChanged?: () => void): void;
|
|
5
|
+
export declare function handleRevokeToken(req: http.IncomingMessage, res: http.ServerResponse, params: Record<string, string>, onAuthChanged?: (affected?: AuthChange) => void): void;
|
|
5
6
|
//# sourceMappingURL=tokens.d.ts.map
|
package/dist/api/tokens.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/api/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/api/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAKvB,OAAO,EAAe,KAAK,UAAU,EAAE,MAAM,0BAA0B,CAAA;AAKvE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,IAAI,CAS1F;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAkD1G;AAED,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,aAAa,GAAE,CAAC,QAAQ,CAAC,EAAE,UAAU,KAAK,IAAe,GACxD,IAAI,CAaN"}
|
package/dist/api/tokens.js
CHANGED
|
@@ -14,10 +14,16 @@ export function handleListTokens(req, res) {
|
|
|
14
14
|
json(res, 200, tokens);
|
|
15
15
|
}
|
|
16
16
|
export async function handleCreateToken(req, res) {
|
|
17
|
+
if (!requireAuth(req, res))
|
|
18
|
+
return;
|
|
19
|
+
const body = await readJson(req);
|
|
20
|
+
// Checked again once the body is in: a request can hold its body back for as long as it likes, and a
|
|
21
|
+
// password reset that commits meanwhile revokes every token of the account and ends its sessions. A
|
|
22
|
+
// token minted from the check made before the wait would outlive that reset. From here to the INSERT
|
|
23
|
+
// nothing awaits, so no reset can land between this check and the write.
|
|
17
24
|
const auth = requireAuth(req, res);
|
|
18
25
|
if (!auth)
|
|
19
26
|
return;
|
|
20
|
-
const body = await readJson(req);
|
|
21
27
|
if (!body.name?.trim())
|
|
22
28
|
return json(res, 400, { error: 'name required' });
|
|
23
29
|
const scopes = (body.scope?.trim() || DEFAULT_SCOPE).split(',').map((s) => s.trim());
|
|
@@ -63,7 +69,7 @@ export function handleRevokeToken(req, res, params, onAuthChanged = () => { }) {
|
|
|
63
69
|
if (result.changes === 0)
|
|
64
70
|
return json(res, 404, { error: 'Token not found' });
|
|
65
71
|
// Closes the sockets that were opened with it.
|
|
66
|
-
onAuthChanged();
|
|
72
|
+
onAuthChanged({ userId: auth.userId, scope: 'token', patId: Number(params.id) });
|
|
67
73
|
json(res, 204, null);
|
|
68
74
|
}
|
|
69
75
|
//# sourceMappingURL=tokens.js.map
|
package/dist/api/tokens.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/api/tokens.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,QAAQ,CAAA;AAC3B,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAChC,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AACzE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,EAAE,WAAW,
|
|
1
|
+
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/api/tokens.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,11 +5,6 @@ export declare function verifyPassword(password: string, stored: string): boolea
|
|
|
5
5
|
*
|
|
6
6
|
* Shared so the two bootstrap paths cannot disagree about what "already initialized" means.
|
|
7
7
|
*
|
|
8
|
-
* They are not identical in every respect: the boot path trims `TAPFLOW_ADMIN_EMAIL` and the HTTP
|
|
9
|
-
* one stores `body.email` as sent, so the same address with surrounding whitespace produces two
|
|
10
|
-
* different rows and the HTTP one never matches `handleLogin`'s `WHERE email = ?`. Normalising
|
|
11
|
-
* inside this function would change what a shipped endpoint stores, so it is a decision rather than
|
|
12
|
-
* a fix here — tracked separately.
|
|
13
8
|
*/
|
|
14
9
|
export declare function isInitialized(): boolean;
|
|
15
10
|
/** Why `createAdminAccount` refused, or `ok`. The caller turns this into an HTTP status or a log line. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adminAccount.d.ts","sourceRoot":"","sources":["../../src/lib/adminAccount.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"adminAccount.d.ts","sourceRoot":"","sources":["../../src/lib/adminAccount.ts"],"names":[],"mappings":"AAiBA,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIzD;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAQxE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAGvC;AAED,0GAA0G;AAC1G,MAAM,MAAM,iBAAiB,GAAG,IAAI,GAAG,gBAAgB,GAAG,oBAAoB,CAAA;AAE9E;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAUrF"}
|
package/dist/lib/adminAccount.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
import { getDb } from '../db.js';
|
|
3
|
+
import { normalizeEmail } from './email.js';
|
|
3
4
|
// The first-owner rules, and the password hashing they need.
|
|
4
5
|
//
|
|
5
6
|
// **Why this is in `lib/` and not beside the HTTP handlers.** Two callers need it: `api/auth.ts`
|
|
@@ -32,11 +33,6 @@ export function verifyPassword(password, stored) {
|
|
|
32
33
|
*
|
|
33
34
|
* Shared so the two bootstrap paths cannot disagree about what "already initialized" means.
|
|
34
35
|
*
|
|
35
|
-
* They are not identical in every respect: the boot path trims `TAPFLOW_ADMIN_EMAIL` and the HTTP
|
|
36
|
-
* one stores `body.email` as sent, so the same address with surrounding whitespace produces two
|
|
37
|
-
* different rows and the HTTP one never matches `handleLogin`'s `WHERE email = ?`. Normalising
|
|
38
|
-
* inside this function would change what a shipped endpoint stores, so it is a decision rather than
|
|
39
|
-
* a fix here — tracked separately.
|
|
40
36
|
*/
|
|
41
37
|
export function isInitialized() {
|
|
42
38
|
const { n } = getDb().prepare('SELECT COUNT(*) as n FROM users').get();
|
|
@@ -49,13 +45,16 @@ export function isInitialized() {
|
|
|
49
45
|
* differently: 403 over HTTP, a silent no-op on every restart at boot.
|
|
50
46
|
*/
|
|
51
47
|
export function createAdminAccount(email, password) {
|
|
52
|
-
|
|
48
|
+
// Normalized here rather than by each caller, so the boot path and `POST /auth/init` store the same
|
|
49
|
+
// value for the same address (#715).
|
|
50
|
+
const stored = email ? normalizeEmail(email) : '';
|
|
51
|
+
if (!stored || !password)
|
|
53
52
|
return 'missing-fields';
|
|
54
53
|
if (password.length < 8)
|
|
55
54
|
return 'password-too-short';
|
|
56
55
|
getDb()
|
|
57
56
|
.prepare('INSERT INTO users (email, display_name, role, password_hash) VALUES (?, ?, ?, ?)')
|
|
58
|
-
.run(
|
|
57
|
+
.run(stored, 'Admin', 'Admin', makePasswordHash(password));
|
|
59
58
|
return 'ok';
|
|
60
59
|
}
|
|
61
60
|
//# sourceMappingURL=adminAccount.js.map
|