matrix-nodejs-homeserver 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -9
- package/dist/api/account.js +109 -64
- package/dist/api/admin.js +9 -0
- package/dist/api/federation.js +1 -1
- package/dist/api/index.js +2 -0
- package/dist/api/misc.js +3 -3
- package/dist/api/oauthserver.js +620 -0
- package/dist/api/sync.js +1 -1
- package/dist/api/threepid.js +1 -1
- package/dist/api/uia.js +20 -8
- package/dist/api/user.js +6 -3
- package/dist/core/auth.d.ts +33 -2
- package/dist/core/auth.js +84 -21
- package/dist/core/oauth.js +2 -1
- package/dist/core/oauthserver.d.ts +141 -0
- package/dist/core/oauthserver.js +603 -0
- package/dist/core/ratelimit.d.ts +1 -1
- package/dist/core/ratelimit.js +4 -0
- package/dist/core/sso.d.ts +51 -15
- package/dist/core/sso.js +86 -26
- package/dist/homeserver.d.ts +12 -1
- package/dist/homeserver.js +17 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/storage/schema.js +20 -2
- package/dist/vendor/zod/v4/classic/errors.js +1 -1
- package/dist/vendor/zod/v4/classic/parse.js +1 -1
- package/dist/vendor/zod/v4/classic/schemas.js +1 -1
- package/dist/vendor/zod/v4/core/api.js +1 -1
- package/dist/vendor/zod/v4/core/checks.js +1 -1
- package/dist/vendor/zod/v4/core/core.js +1 -1
- package/dist/vendor/zod/v4/core/doc.js +1 -1
- package/dist/vendor/zod/v4/core/errors.js +1 -1
- package/dist/vendor/zod/v4/core/json-schema-processors.js +1 -1
- package/dist/vendor/zod/v4/core/memoizer.js +1 -1
- package/dist/vendor/zod/v4/core/parse.js +1 -1
- package/dist/vendor/zod/v4/core/regexes.js +1 -1
- package/dist/vendor/zod/v4/core/registries.js +1 -1
- package/dist/vendor/zod/v4/core/schemas.js +1 -1
- package/dist/vendor/zod/v4/core/to-json-schema.js +1 -1
- package/dist/vendor/zod/v4/core/util.js +1 -1
- package/dist/vendor/zod/v4/core/versions.js +1 -1
- package/dist/vendor/zod/v4/locales/en.js +1 -1
- package/package.json +12 -15
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import { randomToken } from "../core/crypto.js";
|
|
2
|
+
import { M } from "../core/m.js";
|
|
3
|
+
import { MatrixError, errors } from "../core/errors.js";
|
|
4
|
+
import { LOCALPART_RE } from "../core/ids.js";
|
|
5
|
+
import { escapeHtml } from "../core/html.js";
|
|
6
|
+
import { procedure } from "../http/procedure.js";
|
|
7
|
+
import { OAUTH_PREFIX, OAuthError, allowCrossSigningReset } from "../core/oauthserver.js";
|
|
8
|
+
import { legacyClientRedirect } from "../core/sso.js";
|
|
9
|
+
import { setProfileField } from "../core/profile.js";
|
|
10
|
+
import { checkPasswordLogin, createUser, redeemRegistrationToken, registrationFlows, registrationTokenValid, ssoConfirmPage, validateNewUsername } from "./account.js";
|
|
11
|
+
import { timingSafeEqual } from "node:crypto";
|
|
12
|
+
//#region src/api/oauthserver.ts
|
|
13
|
+
const P = OAUTH_PREFIX;
|
|
14
|
+
function server(hs) {
|
|
15
|
+
if (!hs.oauthServer) throw errors.unrecognized("The OAuth 2.0 API is not enabled");
|
|
16
|
+
return hs.oauthServer;
|
|
17
|
+
}
|
|
18
|
+
function cookieName(hs) {
|
|
19
|
+
return secure(hs) ? "__Host-oauth_session" : "oauth_session";
|
|
20
|
+
}
|
|
21
|
+
const secure = (hs) => server(hs).issuer.startsWith("https:");
|
|
22
|
+
const sessionTtl = (hs) => server(hs).options.webSessionLifetimeMs ?? 6048e5;
|
|
23
|
+
function readCookie(ctx, name) {
|
|
24
|
+
for (const part of (ctx.request.headers.get("cookie") ?? "").split(";")) {
|
|
25
|
+
const [k, ...v] = part.trim().split("=");
|
|
26
|
+
if (k === name) return v.join("=");
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
/** The browser's session, if it has one. */
|
|
31
|
+
async function readWebSession(ctx) {
|
|
32
|
+
if (!ctx.hs.oauthServer) return null;
|
|
33
|
+
const sid = readCookie(ctx, cookieName(ctx.hs));
|
|
34
|
+
if (!sid || !/^[A-Za-z0-9_-]{20,100}$/.test(sid)) return null;
|
|
35
|
+
return ctx.hs.cluster.get(`oauth:web:${sid}`);
|
|
36
|
+
}
|
|
37
|
+
/** The browser's session, created (with a cookie to set) when it has none. */
|
|
38
|
+
async function webSession(ctx) {
|
|
39
|
+
const existing = await readWebSession(ctx);
|
|
40
|
+
if (existing) return { web: existing };
|
|
41
|
+
const web = {
|
|
42
|
+
sid: randomToken(32),
|
|
43
|
+
csrf: randomToken(24)
|
|
44
|
+
};
|
|
45
|
+
await ctx.hs.cluster.set(`oauth:web:${web.sid}`, web, sessionTtl(ctx.hs));
|
|
46
|
+
const attrs = [
|
|
47
|
+
`Path=/`,
|
|
48
|
+
`HttpOnly`,
|
|
49
|
+
`SameSite=Lax`,
|
|
50
|
+
`Max-Age=${Math.floor(sessionTtl(ctx.hs) / 1e3)}`
|
|
51
|
+
];
|
|
52
|
+
if (secure(ctx.hs)) attrs.push("Secure");
|
|
53
|
+
return {
|
|
54
|
+
web,
|
|
55
|
+
cookie: `${cookieName(ctx.hs)}=${web.sid}; ${attrs.join("; ")}`
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Log the browser session in (password, registration or an upstream provider). */
|
|
59
|
+
async function setWebUser(hs, web, userId) {
|
|
60
|
+
const next = {
|
|
61
|
+
sid: web.sid,
|
|
62
|
+
csrf: web.csrf
|
|
63
|
+
};
|
|
64
|
+
if (userId) Object.assign(next, {
|
|
65
|
+
userId,
|
|
66
|
+
authTime: Date.now()
|
|
67
|
+
});
|
|
68
|
+
Object.assign(web, next);
|
|
69
|
+
await hs.cluster.set(`oauth:web:${web.sid}`, next, sessionTtl(hs));
|
|
70
|
+
}
|
|
71
|
+
/** A `next` value: a page of this server, nothing else (no open redirect). */
|
|
72
|
+
function safeNext(raw) {
|
|
73
|
+
if (raw && raw.startsWith(`${P}/`) && !raw.includes("//") && !raw.includes("\\") && !raw.includes("..")) return raw;
|
|
74
|
+
return `${P}/account`;
|
|
75
|
+
}
|
|
76
|
+
const pageUrl = (hs, path) => server(hs).url(path.startsWith(P) ? path.slice(P.length) : path);
|
|
77
|
+
function redirect(location, cookie, status = 303) {
|
|
78
|
+
const headers = new Headers({
|
|
79
|
+
location,
|
|
80
|
+
"cache-control": "no-store"
|
|
81
|
+
});
|
|
82
|
+
if (cookie) headers.append("set-cookie", cookie);
|
|
83
|
+
return new Response(null, {
|
|
84
|
+
status,
|
|
85
|
+
headers
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async function readForm(ctx) {
|
|
89
|
+
return new URLSearchParams(new TextDecoder().decode(await ctx.bytes()));
|
|
90
|
+
}
|
|
91
|
+
function checkCsrf(ctx, web, form) {
|
|
92
|
+
const origin = ctx.request.headers.get("origin");
|
|
93
|
+
if (origin && origin !== new URL(server(ctx.hs).issuer).origin) throw errors.forbidden("Cross-site request");
|
|
94
|
+
const given = Buffer.from(form.get("csrf") ?? "");
|
|
95
|
+
const expected = Buffer.from(web.csrf);
|
|
96
|
+
if (given.length !== expected.length || !timingSafeEqual(given, expected)) throw errors.forbidden("The form expired; reload the page");
|
|
97
|
+
}
|
|
98
|
+
const STYLE = `
|
|
99
|
+
:root{--bg:#f6f7f9;--card:#fff;--text:#1b1f24;--muted:#5d6670;--line:#dde1e6;--accent:#0b7a55;--accent-text:#fff;--danger:#b42318}
|
|
100
|
+
@media (prefers-color-scheme:dark){:root{--bg:#111418;--card:#1a1e23;--text:#e8eaed;--muted:#9aa3ad;--line:#2c323a;--accent:#2fb37f;--accent-text:#06120d;--danger:#f97066}}
|
|
101
|
+
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
|
|
102
|
+
main{max-width:30rem;margin:2.5rem auto;padding:0 1rem}.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:1.5rem;margin-bottom:1rem}
|
|
103
|
+
h1{font-size:1.4rem;margin:0 0 .25rem}h2{font-size:1.05rem;margin:0 0 .75rem}p{margin:.5rem 0}.muted{color:var(--muted);font-size:.9rem}
|
|
104
|
+
label{display:block;font-size:.9rem;margin:.75rem 0 .25rem}input[type=text],input[type=password]{width:100%;padding:.6rem .7rem;border:1px solid var(--line);border-radius:8px;background:var(--bg);color:var(--text);font:inherit}
|
|
105
|
+
button,a.button{display:inline-block;margin-top:1rem;padding:.6rem 1.1rem;border-radius:8px;border:1px solid var(--line);background:var(--card);color:var(--text);font:inherit;cursor:pointer;text-decoration:none}
|
|
106
|
+
button.primary,a.button.primary{background:var(--accent);border-color:var(--accent);color:var(--accent-text)}button.danger{color:var(--danger)}
|
|
107
|
+
.row{display:flex;gap:.5rem;flex-wrap:wrap}.error{color:var(--danger)}.notice{border-left:3px solid var(--accent);padding-left:.75rem}
|
|
108
|
+
ul.devices{list-style:none;padding:0;margin:0}ul.devices li{padding:.6rem 0;border-top:1px solid var(--line);display:flex;justify-content:space-between;gap:.5rem;align-items:center}
|
|
109
|
+
ul.devices li.focus{background:color-mix(in srgb,var(--accent) 10%,transparent)}ul.devices button{margin:0}code{font-size:.9em;word-break:break-all}
|
|
110
|
+
.client{display:flex;gap:.75rem;align-items:center}.client img{width:48px;height:48px;border-radius:8px}footer{text-align:center}footer a{color:var(--muted)}`;
|
|
111
|
+
function page(hs, title, body, opts = {}) {
|
|
112
|
+
const o = server(hs).options;
|
|
113
|
+
const links = [o.termsUrl && `<a href="${escapeHtml(o.termsUrl)}">Terms</a>`, o.policyUrl && `<a href="${escapeHtml(o.policyUrl)}">Privacy</a>`].filter(Boolean).join(" · ");
|
|
114
|
+
const html = `<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
115
|
+
<title>${escapeHtml(title)} · ${escapeHtml(hs.serverName)}</title><style>${STYLE}</style>
|
|
116
|
+
<main>${body}${links ? `<footer class="muted">${links}</footer>` : ""}</main></html>`;
|
|
117
|
+
const headers = new Headers({
|
|
118
|
+
"content-type": "text/html; charset=utf-8",
|
|
119
|
+
"cache-control": "no-store",
|
|
120
|
+
"referrer-policy": "same-origin",
|
|
121
|
+
"x-frame-options": "DENY",
|
|
122
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; img-src https:; frame-ancestors 'none'; base-uri 'none'"
|
|
123
|
+
});
|
|
124
|
+
if (opts.cookie) headers.append("set-cookie", opts.cookie);
|
|
125
|
+
return new Response(html, {
|
|
126
|
+
status: opts.status ?? 200,
|
|
127
|
+
headers
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function errorPage(hs, message, status = 400) {
|
|
131
|
+
return page(hs, "Something went wrong", `<div class="card"><h1>Something went wrong</h1><p>${escapeHtml(message)}</p></div>`, { status });
|
|
132
|
+
}
|
|
133
|
+
const csrfField = (web) => `<input type="hidden" name="csrf" value="${escapeHtml(web.csrf)}">`;
|
|
134
|
+
/** Who the client is, as far as its registration says. */
|
|
135
|
+
function clientCard(client) {
|
|
136
|
+
const host = new URL(client.client_uri).host;
|
|
137
|
+
const name = client.client_name ?? host;
|
|
138
|
+
const logo = client.logo_uri ? `<img src="${escapeHtml(client.logo_uri)}" alt="">` : "";
|
|
139
|
+
const docs = [client.tos_uri && `<a href="${escapeHtml(client.tos_uri)}">terms</a>`, client.policy_uri && `<a href="${escapeHtml(client.policy_uri)}">privacy policy</a>`].filter(Boolean);
|
|
140
|
+
return `<div class="client">${logo}<div><b>${escapeHtml(name)}</b><div class="muted"><a href="${escapeHtml(client.client_uri)}">${escapeHtml(host)}</a>${docs.length ? ` · ${docs.join(" · ")}` : ""}</div></div></div>`;
|
|
141
|
+
}
|
|
142
|
+
/** Wraps a page handler: Matrix errors (rate limits, bad input) become an error page. */
|
|
143
|
+
function pageRoute(handler) {
|
|
144
|
+
return procedure({ handler: async (ctx) => {
|
|
145
|
+
server(ctx.hs);
|
|
146
|
+
try {
|
|
147
|
+
return await handler(ctx);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
if (err instanceof MatrixError) return errorPage(ctx.hs, err.message, err.status);
|
|
150
|
+
if (err instanceof OAuthError) return errorPage(ctx.hs, err.description, err.status);
|
|
151
|
+
throw err;
|
|
152
|
+
}
|
|
153
|
+
} });
|
|
154
|
+
}
|
|
155
|
+
/** The logged-in user of the browser, or a redirect to the login page that comes back here. */
|
|
156
|
+
async function loggedIn(ctx, here) {
|
|
157
|
+
const { web, cookie } = await webSession(ctx);
|
|
158
|
+
if (web.userId) {
|
|
159
|
+
const user = await ctx.hs.db.findOne("users", [{
|
|
160
|
+
field: "user_id",
|
|
161
|
+
value: web.userId
|
|
162
|
+
}]);
|
|
163
|
+
if (user && !user.deactivated) {
|
|
164
|
+
if (user.locked) return errorPage(ctx.hs, "This account is locked.", 403);
|
|
165
|
+
return {
|
|
166
|
+
web,
|
|
167
|
+
userId: web.userId,
|
|
168
|
+
cookie
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
await setWebUser(ctx.hs, web, void 0);
|
|
172
|
+
}
|
|
173
|
+
return redirect(`${pageUrl(ctx.hs, "/login")}?next=${encodeURIComponent(here)}`, cookie, 302);
|
|
174
|
+
}
|
|
175
|
+
/** Whether the registration page can create an account now, and whether it needs a token. */
|
|
176
|
+
async function registration(hs) {
|
|
177
|
+
if (!hs.userAuth.passwordLoginEnabled || !await hs.registrationAllowed()) return {
|
|
178
|
+
open: false,
|
|
179
|
+
token: false
|
|
180
|
+
};
|
|
181
|
+
const flows = await registrationFlows(hs);
|
|
182
|
+
if (flows.every((f) => f.includes(M.LOGIN.EMAIL.IDENTITY))) return {
|
|
183
|
+
open: false,
|
|
184
|
+
token: false
|
|
185
|
+
};
|
|
186
|
+
return {
|
|
187
|
+
open: true,
|
|
188
|
+
token: flows.every((f) => f.includes(M.LOGIN.REGISTRATION_TOKEN))
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function providerButtons(hs, next) {
|
|
192
|
+
return (hs.sso?.providers ?? []).map((p) => `<a class="button" href="${escapeHtml(`${pageUrl(hs, `/upstream/${encodeURIComponent(p.id)}`)}?next=${encodeURIComponent(next)}`)}">Continue with ${escapeHtml(p.name)}</a>`).join(" ");
|
|
193
|
+
}
|
|
194
|
+
async function loginPage(ctx, web, next, opts = {}) {
|
|
195
|
+
const hs = ctx.hs;
|
|
196
|
+
const reg = await registration(hs);
|
|
197
|
+
const password = hs.userAuth.passwordLoginEnabled ? `<form method="post" action="${escapeHtml(pageUrl(hs, "/login"))}">${csrfField(web)}<input type="hidden" name="next" value="${escapeHtml(next)}">
|
|
198
|
+
<label for="u">Username</label><input id="u" type="text" name="username" autocomplete="username" autocapitalize="none" required value="${escapeHtml(opts.username ?? "")}">
|
|
199
|
+
<label for="p">Password</label><input id="p" type="password" name="password" autocomplete="current-password" required>
|
|
200
|
+
<button class="primary" type="submit">Sign in</button></form>` : "";
|
|
201
|
+
const providers = providerButtons(hs, next);
|
|
202
|
+
return page(hs, "Sign in", `<div class="card"><h1>Sign in</h1><p class="muted">to <b>${escapeHtml(hs.serverName)}</b></p>
|
|
203
|
+
${opts.error ? `<p class="error">${escapeHtml(opts.error)}</p>` : ""}${password}
|
|
204
|
+
${providers ? `<div class="row">${providers}</div>` : ""}
|
|
205
|
+
${!password && !providers ? "<p>No way to sign in is configured on this server.</p>" : ""}
|
|
206
|
+
${reg.open ? `<p class="muted">No account? <a href="${escapeHtml(`${pageUrl(hs, "/register-account")}?next=${encodeURIComponent(next)}`)}">Create one</a></p>` : ""}</div>`, {
|
|
207
|
+
cookie: opts.cookie,
|
|
208
|
+
status: opts.error ? 401 : 200
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
async function registerPage(ctx, web, next, opts = {}) {
|
|
212
|
+
const hs = ctx.hs;
|
|
213
|
+
const reg = await registration(hs);
|
|
214
|
+
const providers = providerButtons(hs, next);
|
|
215
|
+
if (!reg.open) return page(hs, "Create an account", `<div class="card"><h1>Create an account</h1>${providers ? `<p>Sign in with one of these and an account is created for you:</p><div class="row">${providers}</div>` : "<p>This server does not take new accounts.</p>"}<p class="muted"><a href="${escapeHtml(`${pageUrl(hs, "/login")}?next=${encodeURIComponent(next)}`)}">Sign in instead</a></p></div>`, { cookie: opts.cookie });
|
|
216
|
+
const o = server(hs).options;
|
|
217
|
+
const terms = o.termsUrl || o.policyUrl ? `<p class="muted">By creating an account you accept the terms and privacy policy linked below.</p>` : "";
|
|
218
|
+
return page(hs, "Create an account", `<div class="card"><h1>Create an account</h1><p class="muted">on <b>${escapeHtml(hs.serverName)}</b></p>
|
|
219
|
+
${opts.error ? `<p class="error">${escapeHtml(opts.error)}</p>` : ""}
|
|
220
|
+
<form method="post" action="${escapeHtml(pageUrl(hs, "/register-account"))}">${csrfField(web)}<input type="hidden" name="next" value="${escapeHtml(next)}">
|
|
221
|
+
<label for="u">Username</label><input id="u" type="text" name="username" autocomplete="username" autocapitalize="none" required value="${escapeHtml(opts.username ?? "")}">
|
|
222
|
+
<label for="p">Password</label><input id="p" type="password" name="password" autocomplete="new-password" required>
|
|
223
|
+
${reg.token ? `<label for="t">Registration token</label><input id="t" type="text" name="token" required>` : ""}
|
|
224
|
+
${terms}<button class="primary" type="submit">Create account</button></form>
|
|
225
|
+
${providers ? `<p class="muted">Or sign in with:</p><div class="row">${providers}</div>` : ""}
|
|
226
|
+
<p class="muted"><a href="${escapeHtml(`${pageUrl(hs, "/login")}?next=${encodeURIComponent(next)}`)}">I already have an account</a></p></div>`, {
|
|
227
|
+
cookie: opts.cookie,
|
|
228
|
+
status: opts.error ? 400 : 200
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
/** The approval form shared by the authorization code and device flows. */
|
|
232
|
+
function consentPage(hs, web, opts) {
|
|
233
|
+
const hidden = Object.entries(opts.hidden).map(([k, v]) => `<input type="hidden" name="${escapeHtml(k)}" value="${escapeHtml(v)}">`).join("");
|
|
234
|
+
return page(hs, "Allow access", `<div class="card"><h1>Allow access?</h1>${clientCard(opts.client)}
|
|
235
|
+
<p>This app wants full access to your account <b>${escapeHtml(opts.userId)}</b>: your messages, rooms and keys. It will sign in as the device <code>${escapeHtml(opts.deviceId)}</code>.</p>
|
|
236
|
+
<p class="muted">Only continue if you just started signing in with this app.</p>
|
|
237
|
+
<form method="post" action="${escapeHtml(opts.action)}">${csrfField(web)}${hidden}<div class="row">
|
|
238
|
+
<button class="primary" type="submit" name="decision" value="allow">Allow</button><button type="submit" name="decision" value="deny">Deny</button></div></form></div>
|
|
239
|
+
<form method="post" action="${escapeHtml(pageUrl(hs, "/logout"))}">${csrfField(web)}<input type="hidden" name="next" value="${escapeHtml(opts.here)}">
|
|
240
|
+
<p class="muted">Not ${escapeHtml(opts.userId)}? <button type="submit">Use another account</button></p></form>`, { cookie: opts.cookie });
|
|
241
|
+
}
|
|
242
|
+
function oauthJson(body, status = 200) {
|
|
243
|
+
return new Response(JSON.stringify(body), {
|
|
244
|
+
status,
|
|
245
|
+
headers: {
|
|
246
|
+
"content-type": "application/json",
|
|
247
|
+
"cache-control": "no-store",
|
|
248
|
+
pragma: "no-cache"
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/** OAuth endpoints answer with RFC 6749 errors, not Matrix ones. */
|
|
253
|
+
function oauthEndpoint(handler) {
|
|
254
|
+
return procedure({ handler: async (ctx) => {
|
|
255
|
+
const s = server(ctx.hs);
|
|
256
|
+
try {
|
|
257
|
+
return await handler(ctx, s);
|
|
258
|
+
} catch (err) {
|
|
259
|
+
if (err instanceof OAuthError) return oauthJson({
|
|
260
|
+
error: err.error,
|
|
261
|
+
error_description: err.description
|
|
262
|
+
}, err.status);
|
|
263
|
+
throw err;
|
|
264
|
+
}
|
|
265
|
+
} });
|
|
266
|
+
}
|
|
267
|
+
/** The client of a token request: Basic credentials (RFC 6749 section 2.3.1) or `client_id`. */
|
|
268
|
+
function requestClientId(ctx, form) {
|
|
269
|
+
const header = ctx.request.headers.get("authorization");
|
|
270
|
+
if (header?.startsWith("Basic ")) {
|
|
271
|
+
const decoded = Buffer.from(header.slice(6), "base64").toString("utf8");
|
|
272
|
+
return decodeURIComponent(decoded.split(":")[0]);
|
|
273
|
+
}
|
|
274
|
+
return form.get("client_id");
|
|
275
|
+
}
|
|
276
|
+
const DONE = {
|
|
277
|
+
device_deleted: "The device was signed out.",
|
|
278
|
+
profile: "Your profile was saved.",
|
|
279
|
+
password: "Your password was changed.",
|
|
280
|
+
reset_allowed: "Resetting your identity is allowed for the next 10 minutes. Go back to your app to finish."
|
|
281
|
+
};
|
|
282
|
+
function registerOAuthServerRoutes(r) {
|
|
283
|
+
r.post(`${P}/register`, oauthEndpoint(async (ctx, s) => {
|
|
284
|
+
await ctx.rateLimit("oauthClient");
|
|
285
|
+
let raw;
|
|
286
|
+
try {
|
|
287
|
+
raw = await ctx.json();
|
|
288
|
+
} catch {
|
|
289
|
+
throw new OAuthError("invalid_client_metadata", "The body must be a JSON object");
|
|
290
|
+
}
|
|
291
|
+
return oauthJson(await s.registerClient(raw), 201);
|
|
292
|
+
}));
|
|
293
|
+
r.post(`${P}/token`, oauthEndpoint(async (ctx, s) => {
|
|
294
|
+
const form = await readForm(ctx);
|
|
295
|
+
return oauthJson(await s.token(form, requestClientId(ctx, form)));
|
|
296
|
+
}));
|
|
297
|
+
r.post(`${P}/revoke`, oauthEndpoint(async (ctx, s) => {
|
|
298
|
+
await s.revoke((await readForm(ctx)).get("token"));
|
|
299
|
+
return oauthJson({});
|
|
300
|
+
}));
|
|
301
|
+
r.post(`${P}/device`, oauthEndpoint(async (ctx, s) => {
|
|
302
|
+
await ctx.rateLimit("oauthClient");
|
|
303
|
+
const form = await readForm(ctx);
|
|
304
|
+
return oauthJson(await s.startDeviceGrant(requestClientId(ctx, form), form.get("scope")));
|
|
305
|
+
}));
|
|
306
|
+
r.get(`${P}/keys.json`, procedure({ handler: (ctx) => server(ctx.hs).jwks() }));
|
|
307
|
+
const userinfo = procedure({
|
|
308
|
+
auth: "user",
|
|
309
|
+
handler: (ctx) => ({ sub: ctx.session.userId })
|
|
310
|
+
});
|
|
311
|
+
r.get(`${P}/userinfo`, userinfo);
|
|
312
|
+
r.post(`${P}/userinfo`, userinfo);
|
|
313
|
+
r.get(`${P}/authorize`, pageRoute(async (ctx) => {
|
|
314
|
+
const s = server(ctx.hs);
|
|
315
|
+
let checked;
|
|
316
|
+
try {
|
|
317
|
+
checked = await s.checkAuthorizationRequest(ctx.query);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
if (err instanceof OAuthError) return errorPage(ctx.hs, `The app sent an invalid request: ${err.description}.`);
|
|
320
|
+
throw err;
|
|
321
|
+
}
|
|
322
|
+
if ("error" in checked) return redirect(checked.redirect, void 0, 302);
|
|
323
|
+
const id = await s.saveGrant(checked.grant);
|
|
324
|
+
return redirect(pageUrl(ctx.hs, `/consent/${id}`), void 0, 302);
|
|
325
|
+
}));
|
|
326
|
+
r.get(`${P}/consent/:id`, pageRoute(async (ctx) => {
|
|
327
|
+
const s = server(ctx.hs);
|
|
328
|
+
const grant = await s.grant(ctx.params.id);
|
|
329
|
+
if (!grant) return errorPage(ctx.hs, "This sign-in request expired. Start again from your app.");
|
|
330
|
+
const here = `${P}/consent/${ctx.params.id}`;
|
|
331
|
+
const { web, cookie } = await webSession(ctx);
|
|
332
|
+
if (!web.userId && grant.create && (await registration(ctx.hs)).open) return redirect(`${pageUrl(ctx.hs, "/register-account")}?next=${encodeURIComponent(here)}`, cookie, 302);
|
|
333
|
+
const who = await loggedIn(ctx, here);
|
|
334
|
+
if (who instanceof Response) return who;
|
|
335
|
+
const client = await s.client(grant.clientId);
|
|
336
|
+
if (!client) return errorPage(ctx.hs, "The app is no longer registered. Start again from your app.");
|
|
337
|
+
return consentPage(ctx.hs, who.web, {
|
|
338
|
+
client,
|
|
339
|
+
userId: who.userId,
|
|
340
|
+
deviceId: grant.deviceId,
|
|
341
|
+
action: pageUrl(ctx.hs, here),
|
|
342
|
+
hidden: {},
|
|
343
|
+
here,
|
|
344
|
+
cookie: who.cookie
|
|
345
|
+
});
|
|
346
|
+
}));
|
|
347
|
+
r.post(`${P}/consent/:id`, pageRoute(async (ctx) => {
|
|
348
|
+
const s = server(ctx.hs);
|
|
349
|
+
const form = await readForm(ctx);
|
|
350
|
+
const who = await loggedIn(ctx, `${P}/consent/${ctx.params.id}`);
|
|
351
|
+
if (who instanceof Response) return who;
|
|
352
|
+
checkCsrf(ctx, who.web, form);
|
|
353
|
+
const grant = await s.grant(ctx.params.id);
|
|
354
|
+
if (!grant) return errorPage(ctx.hs, "This sign-in request expired. Start again from your app.");
|
|
355
|
+
if (form.get("decision") !== "allow") return redirect(await s.denyGrant(ctx.params.id, grant));
|
|
356
|
+
return redirect(await s.approveGrant(ctx.params.id, grant, who.userId, who.web.authTime ?? Date.now()));
|
|
357
|
+
}));
|
|
358
|
+
r.get(`${P}/link`, pageRoute(async (ctx) => {
|
|
359
|
+
const s = server(ctx.hs);
|
|
360
|
+
const code = ctx.query.get("code") ?? "";
|
|
361
|
+
const here = `${P}/link${code ? `?code=${encodeURIComponent(code)}` : ""}`;
|
|
362
|
+
const who = await loggedIn(ctx, here);
|
|
363
|
+
if (who instanceof Response) return who;
|
|
364
|
+
const found = code ? await s.deviceGrantByUserCode(code) : null;
|
|
365
|
+
const client = found ? await s.client(found.grant.clientId) : null;
|
|
366
|
+
if (found && client) return consentPage(ctx.hs, who.web, {
|
|
367
|
+
client,
|
|
368
|
+
userId: who.userId,
|
|
369
|
+
deviceId: found.grant.deviceId,
|
|
370
|
+
action: pageUrl(ctx.hs, "/link"),
|
|
371
|
+
hidden: { code: found.grant.userCode },
|
|
372
|
+
here,
|
|
373
|
+
cookie: who.cookie
|
|
374
|
+
});
|
|
375
|
+
const body = `<div class="card"><h1>Link a device</h1><p>Enter the code your device shows.</p>
|
|
376
|
+
${code ? `<p class="error">That code is unknown or expired.</p>` : ""}
|
|
377
|
+
<form method="get" action="${escapeHtml(pageUrl(ctx.hs, "/link"))}"><label for="c">Code</label><input id="c" type="text" name="code" autocomplete="off" autocapitalize="characters" required placeholder="ABCD-EFGH">
|
|
378
|
+
<button class="primary" type="submit">Continue</button></form></div>`;
|
|
379
|
+
return page(ctx.hs, "Link a device", body, { cookie: who.cookie });
|
|
380
|
+
}));
|
|
381
|
+
r.post(`${P}/link`, pageRoute(async (ctx) => {
|
|
382
|
+
const s = server(ctx.hs);
|
|
383
|
+
const form = await readForm(ctx);
|
|
384
|
+
const who = await loggedIn(ctx, `${P}/link`);
|
|
385
|
+
if (who instanceof Response) return who;
|
|
386
|
+
checkCsrf(ctx, who.web, form);
|
|
387
|
+
const found = await s.deviceGrantByUserCode(form.get("code") ?? "");
|
|
388
|
+
if (!found) return errorPage(ctx.hs, "That code is unknown or expired. Start again on your device.");
|
|
389
|
+
const allow = form.get("decision") === "allow";
|
|
390
|
+
await s.decideDeviceGrant(found.deviceCode, found.grant, who.userId, allow);
|
|
391
|
+
const text = allow ? "Your device is signed in. You can close this page." : "Access was denied. You can close this page.";
|
|
392
|
+
return page(ctx.hs, "Link a device", `<div class="card"><h1>${allow ? "Done" : "Denied"}</h1><p>${text}</p></div>`);
|
|
393
|
+
}));
|
|
394
|
+
r.get(`${P}/login`, pageRoute(async (ctx) => {
|
|
395
|
+
const next = safeNext(ctx.query.get("next"));
|
|
396
|
+
const { web, cookie } = await webSession(ctx);
|
|
397
|
+
if (web.userId) return redirect(pageUrl(ctx.hs, next), cookie, 302);
|
|
398
|
+
return loginPage(ctx, web, next, { cookie });
|
|
399
|
+
}));
|
|
400
|
+
r.post(`${P}/login`, pageRoute(async (ctx) => {
|
|
401
|
+
const hs = ctx.hs;
|
|
402
|
+
const form = await readForm(ctx);
|
|
403
|
+
const { web, cookie } = await webSession(ctx);
|
|
404
|
+
checkCsrf(ctx, web, form);
|
|
405
|
+
const next = safeNext(form.get("next"));
|
|
406
|
+
if (!hs.userAuth.passwordLoginEnabled) return errorPage(hs, "Password login is disabled on this server.", 403);
|
|
407
|
+
await hs.rateLimits.check("login", ctx.ip ?? "unknown");
|
|
408
|
+
const username = (form.get("username") ?? "").trim();
|
|
409
|
+
const localpart = (username.startsWith("@") ? username.slice(1).split(":")[0] : username).toLowerCase();
|
|
410
|
+
const userId = `@${localpart}:${hs.serverName}`;
|
|
411
|
+
try {
|
|
412
|
+
if (!LOCALPART_RE.test(localpart) || username.includes(":") && !username.endsWith(`:${hs.serverName}`)) throw errors.forbidden("Invalid username or password");
|
|
413
|
+
await checkPasswordLogin(hs, userId, form.get("password") ?? "", {
|
|
414
|
+
ip: ctx.ip,
|
|
415
|
+
userAgent: ctx.request.headers.get("user-agent")
|
|
416
|
+
});
|
|
417
|
+
} catch (err) {
|
|
418
|
+
if (err instanceof MatrixError) return loginPage(ctx, web, next, {
|
|
419
|
+
error: err.message,
|
|
420
|
+
cookie,
|
|
421
|
+
username
|
|
422
|
+
});
|
|
423
|
+
throw err;
|
|
424
|
+
}
|
|
425
|
+
await hs.audit.record({
|
|
426
|
+
action: "login",
|
|
427
|
+
actor: userId,
|
|
428
|
+
ip: ctx.ip,
|
|
429
|
+
userAgent: ctx.request.headers.get("user-agent"),
|
|
430
|
+
details: { web: true }
|
|
431
|
+
});
|
|
432
|
+
await setWebUser(hs, web, userId);
|
|
433
|
+
return redirect(pageUrl(hs, next), cookie);
|
|
434
|
+
}));
|
|
435
|
+
r.get(`${P}/register-account`, pageRoute(async (ctx) => {
|
|
436
|
+
const next = safeNext(ctx.query.get("next"));
|
|
437
|
+
const { web, cookie } = await webSession(ctx);
|
|
438
|
+
if (web.userId) return redirect(pageUrl(ctx.hs, next), cookie, 302);
|
|
439
|
+
return registerPage(ctx, web, next, { cookie });
|
|
440
|
+
}));
|
|
441
|
+
r.post(`${P}/register-account`, pageRoute(async (ctx) => {
|
|
442
|
+
const hs = ctx.hs;
|
|
443
|
+
const form = await readForm(ctx);
|
|
444
|
+
const { web, cookie } = await webSession(ctx);
|
|
445
|
+
checkCsrf(ctx, web, form);
|
|
446
|
+
const next = safeNext(form.get("next"));
|
|
447
|
+
const reg = await registration(hs);
|
|
448
|
+
if (!reg.open) return errorPage(hs, "Registration is closed.", 403);
|
|
449
|
+
await hs.rateLimits.check("register", ctx.ip ?? "unknown");
|
|
450
|
+
const username = (form.get("username") ?? "").trim().toLowerCase();
|
|
451
|
+
const password = form.get("password") ?? "";
|
|
452
|
+
const token = form.get("token") ?? "";
|
|
453
|
+
let userId;
|
|
454
|
+
try {
|
|
455
|
+
const localpart = await validateNewUsername(hs, username);
|
|
456
|
+
if (hs.appservices.exclusiveUserOwner(`@${localpart}:${hs.serverName}`)) throw errors.exclusive("This username is reserved");
|
|
457
|
+
const problem = await hs.userAuth.validatePassword(password);
|
|
458
|
+
if (problem || !password) throw new MatrixError(400, "M_WEAK_PASSWORD", problem ?? "Choose a password");
|
|
459
|
+
if (reg.token && !await registrationTokenValid(hs, token)) throw errors.forbidden("Invalid registration token");
|
|
460
|
+
if (reg.token) await redeemRegistrationToken(hs, token);
|
|
461
|
+
userId = await createUser(hs, localpart, { password });
|
|
462
|
+
} catch (err) {
|
|
463
|
+
if (err instanceof MatrixError) return registerPage(ctx, web, next, {
|
|
464
|
+
error: err.message,
|
|
465
|
+
cookie,
|
|
466
|
+
username
|
|
467
|
+
});
|
|
468
|
+
throw err;
|
|
469
|
+
}
|
|
470
|
+
await hs.audit.record({
|
|
471
|
+
action: "register",
|
|
472
|
+
actor: userId,
|
|
473
|
+
ip: ctx.ip,
|
|
474
|
+
userAgent: ctx.request.headers.get("user-agent"),
|
|
475
|
+
details: {
|
|
476
|
+
web: true,
|
|
477
|
+
token: reg.token
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
await setWebUser(hs, web, userId);
|
|
481
|
+
return redirect(pageUrl(hs, next), cookie);
|
|
482
|
+
}));
|
|
483
|
+
r.get(`${P}/upstream/:idp`, pageRoute(async (ctx) => {
|
|
484
|
+
const hs = ctx.hs;
|
|
485
|
+
if (!hs.sso?.providers.some((p) => p.id === ctx.params.idp)) return errorPage(hs, "Unknown sign-in provider.", 404);
|
|
486
|
+
const { web, cookie } = await webSession(ctx);
|
|
487
|
+
const next = safeNext(ctx.query.get("next"));
|
|
488
|
+
return redirect(await hs.sso.startLogin(ctx.params.idp, {
|
|
489
|
+
kind: "web",
|
|
490
|
+
next,
|
|
491
|
+
sid: web.sid
|
|
492
|
+
}), cookie, 302);
|
|
493
|
+
}));
|
|
494
|
+
r.post(`${P}/logout`, pageRoute(async (ctx) => {
|
|
495
|
+
const form = await readForm(ctx);
|
|
496
|
+
const { web, cookie } = await webSession(ctx);
|
|
497
|
+
checkCsrf(ctx, web, form);
|
|
498
|
+
await setWebUser(ctx.hs, web, void 0);
|
|
499
|
+
return redirect(`${pageUrl(ctx.hs, "/login")}?next=${encodeURIComponent(safeNext(form.get("next")))}`, cookie);
|
|
500
|
+
}));
|
|
501
|
+
r.get(`${P}/legacy/:id`, pageRoute(async (ctx) => {
|
|
502
|
+
const hs = ctx.hs;
|
|
503
|
+
const key = `oauth:legacy:${ctx.params.id}`;
|
|
504
|
+
const redirectUrl = await hs.cluster.get(key);
|
|
505
|
+
if (!redirectUrl) return errorPage(hs, "This sign-in request expired. Start again from your app.");
|
|
506
|
+
const who = await loggedIn(ctx, `${P}/legacy/${ctx.params.id}`);
|
|
507
|
+
if (who instanceof Response) return who;
|
|
508
|
+
await hs.cluster.delete(key);
|
|
509
|
+
const result = await legacyClientRedirect(hs, who.userId, redirectUrl);
|
|
510
|
+
return result.confirm ? ssoConfirmPage(hs.serverName, result) : redirect(result.url, who.cookie, 302);
|
|
511
|
+
}));
|
|
512
|
+
r.get(`${P}/account`, pageRoute(async (ctx) => {
|
|
513
|
+
const hs = ctx.hs;
|
|
514
|
+
const action = ctx.query.get("action") ?? "";
|
|
515
|
+
const focusDevice = ctx.query.get("device_id") ?? "";
|
|
516
|
+
const who = await loggedIn(ctx, `${P}/account${ctx.url.search}`);
|
|
517
|
+
if (who instanceof Response) return who;
|
|
518
|
+
const { web, userId } = who;
|
|
519
|
+
const user = await hs.db.findOne("users", [{
|
|
520
|
+
field: "user_id",
|
|
521
|
+
value: userId
|
|
522
|
+
}]);
|
|
523
|
+
const devices = await hs.db.findMany("devices", { where: [{
|
|
524
|
+
field: "user_id",
|
|
525
|
+
value: userId
|
|
526
|
+
}] });
|
|
527
|
+
const self = pageUrl(hs, "/account");
|
|
528
|
+
const form = (op, inner) => `<form method="post" action="${escapeHtml(self)}">${csrfField(web)}<input type="hidden" name="op" value="${op}">${inner}</form>`;
|
|
529
|
+
const done = DONE[ctx.query.get("done") ?? ""];
|
|
530
|
+
const sections = [];
|
|
531
|
+
sections.push(`<div class="card"><h1>Your account</h1><p><b>${escapeHtml(userId)}</b></p>${done ? `<p class="notice">${escapeHtml(done)}</p>` : ""}
|
|
532
|
+
${form("profile", `<label for="d">Display name</label><input id="d" type="text" name="displayname" value="${escapeHtml(user.displayname ?? "")}"><button type="submit">Save</button>`)}</div>`);
|
|
533
|
+
if (action === "org.matrix.cross_signing_reset") sections.push(`<div class="card"><h2>Reset your identity</h2><p>Your app asks to replace your cross-signing keys. Do this only if you lost access to your other devices and your recovery key. Other people will see that your identity changed, and you will have to verify your devices again.</p>
|
|
534
|
+
${form("cross_signing_reset", `<button class="primary" type="submit">Allow the reset</button>`)}</div>`);
|
|
535
|
+
const list = devices.sort((a, b) => (b.last_seen_ts ?? 0) - (a.last_seen_ts ?? 0)).map((d) => {
|
|
536
|
+
const seen = d.last_seen_ts ? new Date(d.last_seen_ts).toISOString().slice(0, 16).replace("T", " ") : "never";
|
|
537
|
+
return `<li${d.device_id === focusDevice ? " class=\"focus\"" : ""}><div><b>${escapeHtml(d.display_name ?? d.device_id)}</b><div class="muted"><code>${escapeHtml(d.device_id)}</code> · last seen ${escapeHtml(seen)}${d.last_seen_ip ? ` · ${escapeHtml(d.last_seen_ip)}` : ""}</div></div>${form("delete_device", `<input type="hidden" name="device_id" value="${escapeHtml(d.device_id)}"><button class="danger" type="submit">Sign out</button>`)}</li>`;
|
|
538
|
+
}).join("");
|
|
539
|
+
sections.push(`<div class="card"><h2>Devices</h2>${list ? `<ul class="devices">${list}</ul>` : "<p class=\"muted\">No devices are signed in.</p>"}</div>`);
|
|
540
|
+
if (hs.userAuth.passwordLoginEnabled && user.password_hash) sections.push(`<div class="card"><h2>Change password</h2>${form("password", `<label for="c">Current password</label><input id="c" type="password" name="current" autocomplete="current-password" required>
|
|
541
|
+
<label for="n">New password</label><input id="n" type="password" name="new" autocomplete="new-password" required><button type="submit">Change password</button>`)}</div>`);
|
|
542
|
+
sections.push(action === "org.matrix.account_deactivate" ? `<div class="card"><h2>Deactivate your account</h2><p>This cannot be undone. You leave every room, all devices are signed out, and nobody can use the username <b>${escapeHtml(userId)}</b> again.</p>
|
|
543
|
+
${form("deactivate", `<label for="x">Type your user ID to confirm</label><input id="x" type="text" name="confirm" autocomplete="off" required><button class="danger" type="submit">Deactivate account</button>`)}</div>` : `<p class="muted"><a href="${escapeHtml(`${self}?action=org.matrix.account_deactivate`)}">Deactivate account…</a></p>`);
|
|
544
|
+
sections.push(`<form method="post" action="${escapeHtml(pageUrl(hs, "/logout"))}">${csrfField(web)}<input type="hidden" name="next" value="${P}/account"><button type="submit">Sign out of this page</button></form>`);
|
|
545
|
+
return page(hs, "Your account", sections.join("\n"), { cookie: who.cookie });
|
|
546
|
+
}));
|
|
547
|
+
r.post(`${P}/account`, pageRoute(async (ctx) => {
|
|
548
|
+
const hs = ctx.hs;
|
|
549
|
+
const form = await readForm(ctx);
|
|
550
|
+
const who = await loggedIn(ctx, `${P}/account`);
|
|
551
|
+
if (who instanceof Response) return who;
|
|
552
|
+
checkCsrf(ctx, who.web, form);
|
|
553
|
+
const { userId } = who;
|
|
554
|
+
const back = (done) => redirect(`${pageUrl(hs, "/account")}?done=${done}`);
|
|
555
|
+
switch (form.get("op")) {
|
|
556
|
+
case "profile": {
|
|
557
|
+
const name = (form.get("displayname") ?? "").trim().slice(0, 256);
|
|
558
|
+
await setProfileField(hs, userId, "displayname", name || null);
|
|
559
|
+
return back("profile");
|
|
560
|
+
}
|
|
561
|
+
case "delete_device": {
|
|
562
|
+
const deviceId = form.get("device_id") ?? "";
|
|
563
|
+
if (await hs.db.findOne("devices", [{
|
|
564
|
+
field: "user_id",
|
|
565
|
+
value: userId
|
|
566
|
+
}, {
|
|
567
|
+
field: "device_id",
|
|
568
|
+
value: deviceId
|
|
569
|
+
}])) await hs.auth.deleteDevice(userId, deviceId);
|
|
570
|
+
return back("device_deleted");
|
|
571
|
+
}
|
|
572
|
+
case "cross_signing_reset":
|
|
573
|
+
await allowCrossSigningReset(hs, userId);
|
|
574
|
+
await hs.audit.record({
|
|
575
|
+
action: "admin_action",
|
|
576
|
+
actor: userId,
|
|
577
|
+
target: userId,
|
|
578
|
+
ip: ctx.ip,
|
|
579
|
+
details: { cross_signing_reset_allowed: true }
|
|
580
|
+
});
|
|
581
|
+
return back("reset_allowed");
|
|
582
|
+
case "password": {
|
|
583
|
+
await hs.rateLimits.check("login", ctx.ip ?? "unknown");
|
|
584
|
+
await checkPasswordLogin(hs, userId, form.get("current") ?? "", {
|
|
585
|
+
ip: ctx.ip,
|
|
586
|
+
userAgent: ctx.request.headers.get("user-agent")
|
|
587
|
+
});
|
|
588
|
+
const password = form.get("new") ?? "";
|
|
589
|
+
const problem = await hs.userAuth.validatePassword(password);
|
|
590
|
+
if (problem || !password) throw new MatrixError(400, "M_WEAK_PASSWORD", problem ?? "Choose a password");
|
|
591
|
+
await hs.db.update("users", [{
|
|
592
|
+
field: "user_id",
|
|
593
|
+
value: userId
|
|
594
|
+
}], { password_hash: await hs.userAuth.hashPassword(password) });
|
|
595
|
+
await hs.audit.record({
|
|
596
|
+
action: "password_change",
|
|
597
|
+
actor: userId,
|
|
598
|
+
ip: ctx.ip,
|
|
599
|
+
details: { web: true }
|
|
600
|
+
});
|
|
601
|
+
return back("password");
|
|
602
|
+
}
|
|
603
|
+
case "deactivate":
|
|
604
|
+
if ((form.get("confirm") ?? "").trim() !== userId) throw errors.invalidParam("The user ID you typed does not match");
|
|
605
|
+
await hs.admin.deactivateUser(userId);
|
|
606
|
+
await hs.audit.record({
|
|
607
|
+
action: "deactivate",
|
|
608
|
+
actor: userId,
|
|
609
|
+
target: userId,
|
|
610
|
+
ip: ctx.ip,
|
|
611
|
+
details: { web: true }
|
|
612
|
+
});
|
|
613
|
+
await setWebUser(hs, who.web, void 0);
|
|
614
|
+
return page(hs, "Account deactivated", `<div class="card"><h1>Account deactivated</h1><p>${escapeHtml(userId)} is deactivated.</p></div>`);
|
|
615
|
+
default: throw errors.invalidParam("Unknown action");
|
|
616
|
+
}
|
|
617
|
+
}));
|
|
618
|
+
}
|
|
619
|
+
//#endregion
|
|
620
|
+
export { readWebSession, registerOAuthServerRoutes, setWebUser };
|
package/dist/api/sync.js
CHANGED
|
@@ -9,10 +9,10 @@ import { body } from "../http/input.js";
|
|
|
9
9
|
import { procedure } from "../http/procedure.js";
|
|
10
10
|
import { readBatch } from "../storage/batch.js";
|
|
11
11
|
import { trackedReads } from "../core/changes.js";
|
|
12
|
+
import { fullProfile } from "../core/profile.js";
|
|
12
13
|
import { matchesFilter, roomAllowed, validateSyncFilter } from "./filters.js";
|
|
13
14
|
import { longPoll } from "./longpoll.js";
|
|
14
15
|
import { Semaphore } from "../core/semaphore.js";
|
|
15
|
-
import { fullProfile } from "../core/profile.js";
|
|
16
16
|
//#region src/api/sync.ts
|
|
17
17
|
function parseSyncToken(raw) {
|
|
18
18
|
if (!raw) return null;
|
package/dist/api/threepid.js
CHANGED
|
@@ -89,7 +89,7 @@ function registerThreepidRoutes(r) {
|
|
|
89
89
|
notDelegated(ctx);
|
|
90
90
|
const s = await ctx.auth();
|
|
91
91
|
const body = ctx.input;
|
|
92
|
-
await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId);
|
|
92
|
+
await requireUia(ctx.hs, body, [[M.LOGIN.PASSWORD]], s.userId, { session: s });
|
|
93
93
|
const validated = await ctx.hs.threepids.validated({
|
|
94
94
|
sid: body.sid,
|
|
95
95
|
client_secret: body.client_secret
|