@supacloud/lite 0.8.0 → 0.8.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/CHANGELOG.md +7 -0
- package/dist/cli.js +106 -42
- package/dist/index.js +106 -42
- package/dist/runtime/auth/handler.d.ts +5 -2
- package/dist/runtime/auth/handler.d.ts.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.1](https://github.com/zuohuadong/supacloud/compare/supacloud-lite-v0.8.0...supacloud-lite-v0.8.1) (2026-08-15)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **lite:** complete extended auth compatibility ([#912](https://github.com/zuohuadong/supacloud/issues/912)) ([dd657f5](https://github.com/zuohuadong/supacloud/commit/dd657f5b52f2f3d65c0178f76fd78d027c153c5a))
|
|
9
|
+
|
|
3
10
|
## [0.8.0](https://github.com/zuohuadong/supacloud/compare/supacloud-lite-v0.7.3...supacloud-lite-v0.8.0) (2026-08-15)
|
|
4
11
|
|
|
5
12
|
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { dirname as dirname7, join as join10, resolve as resolve4 } from "path";
|
|
|
8
8
|
// package.json
|
|
9
9
|
var package_default = {
|
|
10
10
|
name: "@supacloud/lite",
|
|
11
|
-
version: "0.8.
|
|
11
|
+
version: "0.8.1",
|
|
12
12
|
description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
|
|
13
13
|
type: "module",
|
|
14
14
|
license: "Apache-2.0",
|
|
@@ -1296,7 +1296,8 @@ class AuthHandler {
|
|
|
1296
1296
|
return await this.oauth.authorize(url);
|
|
1297
1297
|
}
|
|
1298
1298
|
if (path === "callback" && (method === "GET" || method === "POST")) {
|
|
1299
|
-
|
|
1299
|
+
const callbackUrl = method === "POST" ? await this.oauthCallbackUrl(req, url) : url;
|
|
1300
|
+
return await this.oauth.callback(callbackUrl, (userId) => this.oauthSessionTokensFor(userId));
|
|
1300
1301
|
}
|
|
1301
1302
|
if (path.startsWith("admin/"))
|
|
1302
1303
|
return await this.admin(req, ctx, path, method);
|
|
@@ -1306,6 +1307,16 @@ class AuthHandler {
|
|
|
1306
1307
|
return authError(500, "unexpected_failure", msg);
|
|
1307
1308
|
}
|
|
1308
1309
|
}
|
|
1310
|
+
async oauthCallbackUrl(req, url) {
|
|
1311
|
+
const callbackUrl = new URL(url);
|
|
1312
|
+
const form = await req.formData();
|
|
1313
|
+
for (const field of ["code", "state"]) {
|
|
1314
|
+
const value = form.get(field);
|
|
1315
|
+
if (typeof value === "string" && value)
|
|
1316
|
+
callbackUrl.searchParams.set(field, value);
|
|
1317
|
+
}
|
|
1318
|
+
return callbackUrl;
|
|
1319
|
+
}
|
|
1309
1320
|
async signup(req) {
|
|
1310
1321
|
const body = await req.json().catch(() => ({}));
|
|
1311
1322
|
if (!body.email && !body.password) {
|
|
@@ -1383,7 +1394,7 @@ class AuthHandler {
|
|
|
1383
1394
|
if (!userId)
|
|
1384
1395
|
return authError(403, "flow_state_not_found", "invalid or expired auth code");
|
|
1385
1396
|
const ures = await this.db.query(`select * from auth.users where id = $1`, [userId]);
|
|
1386
|
-
return json(200, await this.
|
|
1397
|
+
return json(200, await this.sessionForOAuth(ures.rows[0]));
|
|
1387
1398
|
}
|
|
1388
1399
|
return authError(400, "invalid_grant", `unsupported grant_type: ${grantType}`);
|
|
1389
1400
|
}
|
|
@@ -1428,17 +1439,18 @@ class AuthHandler {
|
|
|
1428
1439
|
sets.push(`is_anonymous = false`);
|
|
1429
1440
|
sets.push(`raw_app_meta_data = coalesce(raw_app_meta_data, '{}'::jsonb) || '{"provider":"email","providers":["email"]}'::jsonb`);
|
|
1430
1441
|
}
|
|
1431
|
-
if (sets.length === 0)
|
|
1432
|
-
return json(200, this.userJson(user));
|
|
1433
|
-
params.push(user.id);
|
|
1434
|
-
const res = await this.db.query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
|
|
1435
|
-
const updated = res.rows[0];
|
|
1436
|
-
if (upgradingAnon) {
|
|
1437
|
-
await this.db.query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
|
|
1438
|
-
values ($1, 'email', $2, $3)
|
|
1439
|
-
on conflict (provider, provider_id) do nothing`, [updated.id, updated.id, JSON.stringify({ sub: updated.id, email: updated.email })]);
|
|
1442
|
+
if (sets.length === 0) {
|
|
1443
|
+
return json(200, this.userJson(user, await this.getUserFactors(user.id), await this.getUserIdentities(user.id)));
|
|
1440
1444
|
}
|
|
1441
|
-
|
|
1445
|
+
params.push(user.id);
|
|
1446
|
+
const payload = await this.db.transaction(async (query) => {
|
|
1447
|
+
const res = await query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
|
|
1448
|
+
const updated = res.rows[0];
|
|
1449
|
+
if (body.email)
|
|
1450
|
+
await this.ensureEmailIdentity(updated, query);
|
|
1451
|
+
return this.userJsonWithRelations(updated, query);
|
|
1452
|
+
});
|
|
1453
|
+
return json(200, payload);
|
|
1442
1454
|
}
|
|
1443
1455
|
async logout(req, url) {
|
|
1444
1456
|
const claims = await this.claimsFromBearer(req);
|
|
@@ -1468,7 +1480,7 @@ class AuthHandler {
|
|
|
1468
1480
|
let user = res.rows[0];
|
|
1469
1481
|
if (!user) {
|
|
1470
1482
|
if (!createUser)
|
|
1471
|
-
return
|
|
1483
|
+
return json(200, {});
|
|
1472
1484
|
if (this.settings.disableSignup)
|
|
1473
1485
|
return authError(422, "signup_disabled", "Signups not allowed for this instance");
|
|
1474
1486
|
res = await this.db.query(`insert into auth.users (aud, role, email, raw_app_meta_data, raw_user_meta_data)
|
|
@@ -1800,6 +1812,8 @@ Or sign in with this link: ${link}`
|
|
|
1800
1812
|
}
|
|
1801
1813
|
const idMatch = path.match(/^admin\/users\/([0-9a-f-]{36})$/);
|
|
1802
1814
|
const exportMatch = path.match(/^admin\/users\/([0-9a-f-]{36})\/export$/);
|
|
1815
|
+
const factorsMatch = path.match(/^admin\/users\/([0-9a-f-]{36})\/factors$/);
|
|
1816
|
+
const factorMatch = path.match(/^admin\/users\/([0-9a-f-]{36})\/factors\/([0-9a-f-]{36})$/);
|
|
1803
1817
|
if (path === "admin/generate_link" && method === "POST") {
|
|
1804
1818
|
return await this.generateAdminMagicLink(req);
|
|
1805
1819
|
}
|
|
@@ -1811,32 +1825,49 @@ Or sign in with this link: ${link}`
|
|
|
1811
1825
|
if (exportMatch && method === "GET") {
|
|
1812
1826
|
return await this.exportUser(exportMatch[1]);
|
|
1813
1827
|
}
|
|
1828
|
+
if (factorsMatch && method === "GET") {
|
|
1829
|
+
const user = await this.db.query(`select 1 from auth.users where id = $1`, [factorsMatch[1]]);
|
|
1830
|
+
if (user.rows.length === 0)
|
|
1831
|
+
return authError(404, "user_not_found", "User not found");
|
|
1832
|
+
return json(200, await this.getUserFactors(factorsMatch[1]));
|
|
1833
|
+
}
|
|
1834
|
+
if (factorMatch && method === "DELETE") {
|
|
1835
|
+
const deleted = await this.db.query(`delete from auth.mfa_factors where user_id = $1 and id = $2 returning id`, [factorMatch[1], factorMatch[2]]);
|
|
1836
|
+
if (deleted.rows.length === 0)
|
|
1837
|
+
return authError(404, "mfa_factor_not_found", "MFA factor not found");
|
|
1838
|
+
return json(200, { id: factorMatch[2] });
|
|
1839
|
+
}
|
|
1814
1840
|
if (path === "admin/users" && method === "GET") {
|
|
1815
1841
|
const res = await this.db.query(`select * from auth.users order by created_at desc limit 1000`);
|
|
1816
|
-
return json(200, { users: res.rows.map((
|
|
1842
|
+
return json(200, { users: res.rows.map((user) => this.userJson(user)), aud: "authenticated" });
|
|
1817
1843
|
}
|
|
1818
1844
|
if (path === "admin/users" && method === "POST") {
|
|
1819
1845
|
const body = await req.json().catch(() => ({}));
|
|
1820
1846
|
if (!body.email)
|
|
1821
1847
|
return authError(400, "validation_failed", "email is required");
|
|
1822
1848
|
const hashed = body.password ? await hashPassword(body.password) : null;
|
|
1823
|
-
const
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1849
|
+
const created = await this.db.transaction(async (query) => {
|
|
1850
|
+
const res = await query(`insert into auth.users
|
|
1851
|
+
(aud, role, email, encrypted_password, email_confirmed_at, raw_app_meta_data, raw_user_meta_data)
|
|
1852
|
+
values ('authenticated', 'authenticated', $1, $2, case when $3 then now() else null end, $4, $5)
|
|
1853
|
+
returning *`, [
|
|
1854
|
+
body.email.toLowerCase().trim(),
|
|
1855
|
+
hashed,
|
|
1856
|
+
body.email_confirm ?? true,
|
|
1857
|
+
JSON.stringify({ provider: "email", providers: ["email"], ...body.app_metadata ?? {} }),
|
|
1858
|
+
JSON.stringify(body.user_metadata ?? {})
|
|
1859
|
+
]);
|
|
1860
|
+
const user = res.rows[0];
|
|
1861
|
+
await this.ensureEmailIdentity(user, query);
|
|
1862
|
+
return this.userJsonWithRelations(user, query);
|
|
1863
|
+
});
|
|
1864
|
+
return json(200, created);
|
|
1834
1865
|
}
|
|
1835
1866
|
if (idMatch && method === "GET") {
|
|
1836
1867
|
const res = await this.db.query(`select * from auth.users where id = $1`, [idMatch[1]]);
|
|
1837
1868
|
if (res.rows.length === 0)
|
|
1838
1869
|
return authError(404, "user_not_found", "User not found");
|
|
1839
|
-
return json(200, this.
|
|
1870
|
+
return json(200, await this.userJsonWithRelations(res.rows[0]));
|
|
1840
1871
|
}
|
|
1841
1872
|
if (idMatch && method === "PUT") {
|
|
1842
1873
|
const body = await req.json().catch(() => ({}));
|
|
@@ -1863,10 +1894,18 @@ Or sign in with this link: ${link}`
|
|
|
1863
1894
|
if (sets.length === 0)
|
|
1864
1895
|
return authError(400, "validation_failed", "nothing to update");
|
|
1865
1896
|
params.push(idMatch[1]);
|
|
1866
|
-
const
|
|
1867
|
-
|
|
1897
|
+
const updated = await this.db.transaction(async (query) => {
|
|
1898
|
+
const res = await query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
|
|
1899
|
+
const user = res.rows[0];
|
|
1900
|
+
if (!user)
|
|
1901
|
+
return null;
|
|
1902
|
+
if (typeof body.email === "string")
|
|
1903
|
+
await this.ensureEmailIdentity(user, query);
|
|
1904
|
+
return this.userJsonWithRelations(user, query);
|
|
1905
|
+
});
|
|
1906
|
+
if (!updated)
|
|
1868
1907
|
return authError(404, "user_not_found", "User not found");
|
|
1869
|
-
return json(200,
|
|
1908
|
+
return json(200, updated);
|
|
1870
1909
|
}
|
|
1871
1910
|
if (idMatch && method === "DELETE") {
|
|
1872
1911
|
return await this.eraseUser(idMatch[1]);
|
|
@@ -2062,17 +2101,26 @@ Or sign in with this link: ${link}`
|
|
|
2062
2101
|
if (!await verifyTotp(factor.secret, body.code ?? "")) {
|
|
2063
2102
|
return authError(422, "mfa_verification_failed", "Invalid TOTP code entered");
|
|
2064
2103
|
}
|
|
2065
|
-
await this.db.
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2104
|
+
const session = await this.db.transaction(async (query) => {
|
|
2105
|
+
const claimed = await query(`update auth.mfa_challenges set verified_at = now()
|
|
2106
|
+
where id = $1 and factor_id = $2 and verified_at is null and expires_at >= now()
|
|
2107
|
+
returning id`, [challenge.id, factorId]);
|
|
2108
|
+
if (claimed.rows.length === 0)
|
|
2109
|
+
return null;
|
|
2110
|
+
if (factor.status !== "verified") {
|
|
2111
|
+
await query(`update auth.mfa_factors set status = 'verified', updated_at = now() where id = $1`, [factorId]);
|
|
2112
|
+
}
|
|
2113
|
+
return this.sessionFor(user, undefined, {
|
|
2114
|
+
aal: "aal2",
|
|
2115
|
+
amr: [
|
|
2116
|
+
{ method: "password", timestamp: Math.floor(Date.now() / 1000) },
|
|
2117
|
+
{ method: "totp", timestamp: Math.floor(Date.now() / 1000) }
|
|
2118
|
+
]
|
|
2119
|
+
}, query);
|
|
2075
2120
|
});
|
|
2121
|
+
if (!session) {
|
|
2122
|
+
return authError(422, "mfa_verification_failed", "This challenge has already been verified");
|
|
2123
|
+
}
|
|
2076
2124
|
return json(200, session);
|
|
2077
2125
|
}
|
|
2078
2126
|
async unenrollFactor(req, factorId) {
|
|
@@ -2110,6 +2158,17 @@ Or sign in with this link: ${link}`
|
|
|
2110
2158
|
updated_at: iso(r.updated_at)
|
|
2111
2159
|
}));
|
|
2112
2160
|
}
|
|
2161
|
+
async ensureEmailIdentity(user, query = (sql, params) => this.db.query(sql, params)) {
|
|
2162
|
+
if (!user.email)
|
|
2163
|
+
return;
|
|
2164
|
+
await query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
|
|
2165
|
+
values ($1::uuid, 'email', $1::text, $2::jsonb)
|
|
2166
|
+
on conflict (provider, provider_id) do update
|
|
2167
|
+
set identity_data = excluded.identity_data, updated_at = now()`, [user.id, JSON.stringify({ sub: user.id, email: user.email })]);
|
|
2168
|
+
}
|
|
2169
|
+
async userJsonWithRelations(user, query = (sql, params) => this.db.query(sql, params)) {
|
|
2170
|
+
return this.userJson(user, await this.getUserFactors(user.id, query), await this.getUserIdentities(user.id, query));
|
|
2171
|
+
}
|
|
2113
2172
|
async rotateRefreshToken(token) {
|
|
2114
2173
|
return this.db.transaction(async (query) => {
|
|
2115
2174
|
const claimedToken = await this.claimRefreshToken(query, token);
|
|
@@ -2183,9 +2242,14 @@ Or sign in with this link: ${link}`
|
|
|
2183
2242
|
is_anonymous: u.is_anonymous ?? false
|
|
2184
2243
|
};
|
|
2185
2244
|
}
|
|
2186
|
-
|
|
2245
|
+
sessionForOAuth(user) {
|
|
2246
|
+
return this.sessionFor(user, undefined, {
|
|
2247
|
+
amr: [{ method: "oauth", timestamp: Math.floor(Date.now() / 1000) }]
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
async oauthSessionTokensFor(userId) {
|
|
2187
2251
|
const res = await this.db.query(`select * from auth.users where id = $1`, [userId]);
|
|
2188
|
-
const session = await this.
|
|
2252
|
+
const session = await this.sessionForOAuth(res.rows[0]);
|
|
2189
2253
|
return { access_token: session.access_token, refresh_token: session.refresh_token, expires_in: session.expires_in };
|
|
2190
2254
|
}
|
|
2191
2255
|
async sessionFor(user, parentToken, opts, query = (sql, params) => this.db.query(sql, params)) {
|
package/dist/index.js
CHANGED
|
@@ -85,7 +85,7 @@ function randomToken(bytes = 32) {
|
|
|
85
85
|
// package.json
|
|
86
86
|
var package_default = {
|
|
87
87
|
name: "@supacloud/lite",
|
|
88
|
-
version: "0.8.
|
|
88
|
+
version: "0.8.1",
|
|
89
89
|
description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
|
|
90
90
|
type: "module",
|
|
91
91
|
license: "Apache-2.0",
|
|
@@ -1301,7 +1301,8 @@ class AuthHandler {
|
|
|
1301
1301
|
return await this.oauth.authorize(url);
|
|
1302
1302
|
}
|
|
1303
1303
|
if (path === "callback" && (method === "GET" || method === "POST")) {
|
|
1304
|
-
|
|
1304
|
+
const callbackUrl = method === "POST" ? await this.oauthCallbackUrl(req, url) : url;
|
|
1305
|
+
return await this.oauth.callback(callbackUrl, (userId) => this.oauthSessionTokensFor(userId));
|
|
1305
1306
|
}
|
|
1306
1307
|
if (path.startsWith("admin/"))
|
|
1307
1308
|
return await this.admin(req, ctx, path, method);
|
|
@@ -1311,6 +1312,16 @@ class AuthHandler {
|
|
|
1311
1312
|
return authError(500, "unexpected_failure", msg);
|
|
1312
1313
|
}
|
|
1313
1314
|
}
|
|
1315
|
+
async oauthCallbackUrl(req, url) {
|
|
1316
|
+
const callbackUrl = new URL(url);
|
|
1317
|
+
const form = await req.formData();
|
|
1318
|
+
for (const field of ["code", "state"]) {
|
|
1319
|
+
const value = form.get(field);
|
|
1320
|
+
if (typeof value === "string" && value)
|
|
1321
|
+
callbackUrl.searchParams.set(field, value);
|
|
1322
|
+
}
|
|
1323
|
+
return callbackUrl;
|
|
1324
|
+
}
|
|
1314
1325
|
async signup(req) {
|
|
1315
1326
|
const body = await req.json().catch(() => ({}));
|
|
1316
1327
|
if (!body.email && !body.password) {
|
|
@@ -1388,7 +1399,7 @@ class AuthHandler {
|
|
|
1388
1399
|
if (!userId)
|
|
1389
1400
|
return authError(403, "flow_state_not_found", "invalid or expired auth code");
|
|
1390
1401
|
const ures = await this.db.query(`select * from auth.users where id = $1`, [userId]);
|
|
1391
|
-
return json(200, await this.
|
|
1402
|
+
return json(200, await this.sessionForOAuth(ures.rows[0]));
|
|
1392
1403
|
}
|
|
1393
1404
|
return authError(400, "invalid_grant", `unsupported grant_type: ${grantType}`);
|
|
1394
1405
|
}
|
|
@@ -1433,17 +1444,18 @@ class AuthHandler {
|
|
|
1433
1444
|
sets.push(`is_anonymous = false`);
|
|
1434
1445
|
sets.push(`raw_app_meta_data = coalesce(raw_app_meta_data, '{}'::jsonb) || '{"provider":"email","providers":["email"]}'::jsonb`);
|
|
1435
1446
|
}
|
|
1436
|
-
if (sets.length === 0)
|
|
1437
|
-
return json(200, this.userJson(user));
|
|
1438
|
-
params.push(user.id);
|
|
1439
|
-
const res = await this.db.query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
|
|
1440
|
-
const updated = res.rows[0];
|
|
1441
|
-
if (upgradingAnon) {
|
|
1442
|
-
await this.db.query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
|
|
1443
|
-
values ($1, 'email', $2, $3)
|
|
1444
|
-
on conflict (provider, provider_id) do nothing`, [updated.id, updated.id, JSON.stringify({ sub: updated.id, email: updated.email })]);
|
|
1447
|
+
if (sets.length === 0) {
|
|
1448
|
+
return json(200, this.userJson(user, await this.getUserFactors(user.id), await this.getUserIdentities(user.id)));
|
|
1445
1449
|
}
|
|
1446
|
-
|
|
1450
|
+
params.push(user.id);
|
|
1451
|
+
const payload = await this.db.transaction(async (query) => {
|
|
1452
|
+
const res = await query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
|
|
1453
|
+
const updated = res.rows[0];
|
|
1454
|
+
if (body.email)
|
|
1455
|
+
await this.ensureEmailIdentity(updated, query);
|
|
1456
|
+
return this.userJsonWithRelations(updated, query);
|
|
1457
|
+
});
|
|
1458
|
+
return json(200, payload);
|
|
1447
1459
|
}
|
|
1448
1460
|
async logout(req, url) {
|
|
1449
1461
|
const claims = await this.claimsFromBearer(req);
|
|
@@ -1473,7 +1485,7 @@ class AuthHandler {
|
|
|
1473
1485
|
let user = res.rows[0];
|
|
1474
1486
|
if (!user) {
|
|
1475
1487
|
if (!createUser)
|
|
1476
|
-
return
|
|
1488
|
+
return json(200, {});
|
|
1477
1489
|
if (this.settings.disableSignup)
|
|
1478
1490
|
return authError(422, "signup_disabled", "Signups not allowed for this instance");
|
|
1479
1491
|
res = await this.db.query(`insert into auth.users (aud, role, email, raw_app_meta_data, raw_user_meta_data)
|
|
@@ -1805,6 +1817,8 @@ Or sign in with this link: ${link}`
|
|
|
1805
1817
|
}
|
|
1806
1818
|
const idMatch = path.match(/^admin\/users\/([0-9a-f-]{36})$/);
|
|
1807
1819
|
const exportMatch = path.match(/^admin\/users\/([0-9a-f-]{36})\/export$/);
|
|
1820
|
+
const factorsMatch = path.match(/^admin\/users\/([0-9a-f-]{36})\/factors$/);
|
|
1821
|
+
const factorMatch = path.match(/^admin\/users\/([0-9a-f-]{36})\/factors\/([0-9a-f-]{36})$/);
|
|
1808
1822
|
if (path === "admin/generate_link" && method === "POST") {
|
|
1809
1823
|
return await this.generateAdminMagicLink(req);
|
|
1810
1824
|
}
|
|
@@ -1816,32 +1830,49 @@ Or sign in with this link: ${link}`
|
|
|
1816
1830
|
if (exportMatch && method === "GET") {
|
|
1817
1831
|
return await this.exportUser(exportMatch[1]);
|
|
1818
1832
|
}
|
|
1833
|
+
if (factorsMatch && method === "GET") {
|
|
1834
|
+
const user = await this.db.query(`select 1 from auth.users where id = $1`, [factorsMatch[1]]);
|
|
1835
|
+
if (user.rows.length === 0)
|
|
1836
|
+
return authError(404, "user_not_found", "User not found");
|
|
1837
|
+
return json(200, await this.getUserFactors(factorsMatch[1]));
|
|
1838
|
+
}
|
|
1839
|
+
if (factorMatch && method === "DELETE") {
|
|
1840
|
+
const deleted = await this.db.query(`delete from auth.mfa_factors where user_id = $1 and id = $2 returning id`, [factorMatch[1], factorMatch[2]]);
|
|
1841
|
+
if (deleted.rows.length === 0)
|
|
1842
|
+
return authError(404, "mfa_factor_not_found", "MFA factor not found");
|
|
1843
|
+
return json(200, { id: factorMatch[2] });
|
|
1844
|
+
}
|
|
1819
1845
|
if (path === "admin/users" && method === "GET") {
|
|
1820
1846
|
const res = await this.db.query(`select * from auth.users order by created_at desc limit 1000`);
|
|
1821
|
-
return json(200, { users: res.rows.map((
|
|
1847
|
+
return json(200, { users: res.rows.map((user) => this.userJson(user)), aud: "authenticated" });
|
|
1822
1848
|
}
|
|
1823
1849
|
if (path === "admin/users" && method === "POST") {
|
|
1824
1850
|
const body = await req.json().catch(() => ({}));
|
|
1825
1851
|
if (!body.email)
|
|
1826
1852
|
return authError(400, "validation_failed", "email is required");
|
|
1827
1853
|
const hashed = body.password ? await hashPassword(body.password) : null;
|
|
1828
|
-
const
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1854
|
+
const created = await this.db.transaction(async (query) => {
|
|
1855
|
+
const res = await query(`insert into auth.users
|
|
1856
|
+
(aud, role, email, encrypted_password, email_confirmed_at, raw_app_meta_data, raw_user_meta_data)
|
|
1857
|
+
values ('authenticated', 'authenticated', $1, $2, case when $3 then now() else null end, $4, $5)
|
|
1858
|
+
returning *`, [
|
|
1859
|
+
body.email.toLowerCase().trim(),
|
|
1860
|
+
hashed,
|
|
1861
|
+
body.email_confirm ?? true,
|
|
1862
|
+
JSON.stringify({ provider: "email", providers: ["email"], ...body.app_metadata ?? {} }),
|
|
1863
|
+
JSON.stringify(body.user_metadata ?? {})
|
|
1864
|
+
]);
|
|
1865
|
+
const user = res.rows[0];
|
|
1866
|
+
await this.ensureEmailIdentity(user, query);
|
|
1867
|
+
return this.userJsonWithRelations(user, query);
|
|
1868
|
+
});
|
|
1869
|
+
return json(200, created);
|
|
1839
1870
|
}
|
|
1840
1871
|
if (idMatch && method === "GET") {
|
|
1841
1872
|
const res = await this.db.query(`select * from auth.users where id = $1`, [idMatch[1]]);
|
|
1842
1873
|
if (res.rows.length === 0)
|
|
1843
1874
|
return authError(404, "user_not_found", "User not found");
|
|
1844
|
-
return json(200, this.
|
|
1875
|
+
return json(200, await this.userJsonWithRelations(res.rows[0]));
|
|
1845
1876
|
}
|
|
1846
1877
|
if (idMatch && method === "PUT") {
|
|
1847
1878
|
const body = await req.json().catch(() => ({}));
|
|
@@ -1868,10 +1899,18 @@ Or sign in with this link: ${link}`
|
|
|
1868
1899
|
if (sets.length === 0)
|
|
1869
1900
|
return authError(400, "validation_failed", "nothing to update");
|
|
1870
1901
|
params.push(idMatch[1]);
|
|
1871
|
-
const
|
|
1872
|
-
|
|
1902
|
+
const updated = await this.db.transaction(async (query) => {
|
|
1903
|
+
const res = await query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
|
|
1904
|
+
const user = res.rows[0];
|
|
1905
|
+
if (!user)
|
|
1906
|
+
return null;
|
|
1907
|
+
if (typeof body.email === "string")
|
|
1908
|
+
await this.ensureEmailIdentity(user, query);
|
|
1909
|
+
return this.userJsonWithRelations(user, query);
|
|
1910
|
+
});
|
|
1911
|
+
if (!updated)
|
|
1873
1912
|
return authError(404, "user_not_found", "User not found");
|
|
1874
|
-
return json(200,
|
|
1913
|
+
return json(200, updated);
|
|
1875
1914
|
}
|
|
1876
1915
|
if (idMatch && method === "DELETE") {
|
|
1877
1916
|
return await this.eraseUser(idMatch[1]);
|
|
@@ -2067,17 +2106,26 @@ Or sign in with this link: ${link}`
|
|
|
2067
2106
|
if (!await verifyTotp(factor.secret, body.code ?? "")) {
|
|
2068
2107
|
return authError(422, "mfa_verification_failed", "Invalid TOTP code entered");
|
|
2069
2108
|
}
|
|
2070
|
-
await this.db.
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2109
|
+
const session = await this.db.transaction(async (query) => {
|
|
2110
|
+
const claimed = await query(`update auth.mfa_challenges set verified_at = now()
|
|
2111
|
+
where id = $1 and factor_id = $2 and verified_at is null and expires_at >= now()
|
|
2112
|
+
returning id`, [challenge.id, factorId]);
|
|
2113
|
+
if (claimed.rows.length === 0)
|
|
2114
|
+
return null;
|
|
2115
|
+
if (factor.status !== "verified") {
|
|
2116
|
+
await query(`update auth.mfa_factors set status = 'verified', updated_at = now() where id = $1`, [factorId]);
|
|
2117
|
+
}
|
|
2118
|
+
return this.sessionFor(user, undefined, {
|
|
2119
|
+
aal: "aal2",
|
|
2120
|
+
amr: [
|
|
2121
|
+
{ method: "password", timestamp: Math.floor(Date.now() / 1000) },
|
|
2122
|
+
{ method: "totp", timestamp: Math.floor(Date.now() / 1000) }
|
|
2123
|
+
]
|
|
2124
|
+
}, query);
|
|
2080
2125
|
});
|
|
2126
|
+
if (!session) {
|
|
2127
|
+
return authError(422, "mfa_verification_failed", "This challenge has already been verified");
|
|
2128
|
+
}
|
|
2081
2129
|
return json(200, session);
|
|
2082
2130
|
}
|
|
2083
2131
|
async unenrollFactor(req, factorId) {
|
|
@@ -2115,6 +2163,17 @@ Or sign in with this link: ${link}`
|
|
|
2115
2163
|
updated_at: iso(r.updated_at)
|
|
2116
2164
|
}));
|
|
2117
2165
|
}
|
|
2166
|
+
async ensureEmailIdentity(user, query = (sql, params) => this.db.query(sql, params)) {
|
|
2167
|
+
if (!user.email)
|
|
2168
|
+
return;
|
|
2169
|
+
await query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
|
|
2170
|
+
values ($1::uuid, 'email', $1::text, $2::jsonb)
|
|
2171
|
+
on conflict (provider, provider_id) do update
|
|
2172
|
+
set identity_data = excluded.identity_data, updated_at = now()`, [user.id, JSON.stringify({ sub: user.id, email: user.email })]);
|
|
2173
|
+
}
|
|
2174
|
+
async userJsonWithRelations(user, query = (sql, params) => this.db.query(sql, params)) {
|
|
2175
|
+
return this.userJson(user, await this.getUserFactors(user.id, query), await this.getUserIdentities(user.id, query));
|
|
2176
|
+
}
|
|
2118
2177
|
async rotateRefreshToken(token) {
|
|
2119
2178
|
return this.db.transaction(async (query) => {
|
|
2120
2179
|
const claimedToken = await this.claimRefreshToken(query, token);
|
|
@@ -2188,9 +2247,14 @@ Or sign in with this link: ${link}`
|
|
|
2188
2247
|
is_anonymous: u.is_anonymous ?? false
|
|
2189
2248
|
};
|
|
2190
2249
|
}
|
|
2191
|
-
|
|
2250
|
+
sessionForOAuth(user) {
|
|
2251
|
+
return this.sessionFor(user, undefined, {
|
|
2252
|
+
amr: [{ method: "oauth", timestamp: Math.floor(Date.now() / 1000) }]
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
async oauthSessionTokensFor(userId) {
|
|
2192
2256
|
const res = await this.db.query(`select * from auth.users where id = $1`, [userId]);
|
|
2193
|
-
const session = await this.
|
|
2257
|
+
const session = await this.sessionForOAuth(res.rows[0]);
|
|
2194
2258
|
return { access_token: session.access_token, refresh_token: session.refresh_token, expires_in: session.expires_in };
|
|
2195
2259
|
}
|
|
2196
2260
|
async sessionFor(user, parentToken, opts, query = (sql, params) => this.db.query(sql, params)) {
|
|
@@ -97,6 +97,7 @@ export declare class AuthHandler {
|
|
|
97
97
|
private phoneDeliveriesSettledBeforeDeadline;
|
|
98
98
|
/** Dispatch one `/auth/v1/*` request. Any thrown error becomes a 500 `unexpected_failure`. */
|
|
99
99
|
handle(req: Request, ctx: RequestContext, url: URL): Promise<Response>;
|
|
100
|
+
private oauthCallbackUrl;
|
|
100
101
|
private signup;
|
|
101
102
|
private token;
|
|
102
103
|
private getUser;
|
|
@@ -159,6 +160,8 @@ export declare class AuthHandler {
|
|
|
159
160
|
private getUserFactors;
|
|
160
161
|
/** GoTrue-shaped identities for a user (linked providers), from auth.identities. */
|
|
161
162
|
private getUserIdentities;
|
|
163
|
+
private ensureEmailIdentity;
|
|
164
|
+
private userJsonWithRelations;
|
|
162
165
|
private rotateRefreshToken;
|
|
163
166
|
private claimRefreshToken;
|
|
164
167
|
private refreshSessionStartedAt;
|
|
@@ -167,8 +170,8 @@ export declare class AuthHandler {
|
|
|
167
170
|
private claimsFromBearer;
|
|
168
171
|
/** Shape a user row into the GoTrue user object supabase-js expects. */
|
|
169
172
|
userJson(u: UserRow, factors?: Record<string, unknown>[], identities?: Record<string, unknown>[]): Record<string, unknown>;
|
|
170
|
-
|
|
171
|
-
private
|
|
173
|
+
private sessionForOAuth;
|
|
174
|
+
private oauthSessionTokensFor;
|
|
172
175
|
private sessionFor;
|
|
173
176
|
}
|
|
174
177
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/runtime/auth/handler.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/runtime/auth/handler.ts"],"names":[],"mappings":"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"}
|