@fonderie/auth 7.3.6 → 7.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/brain/outcomes.md +2 -0
- package/brain/signatures.md +5 -2
- package/dist/dtos/user.cjs +2 -0
- package/dist/dtos/user.cjs.map +1 -1
- package/dist/dtos/user.d.cts +2 -0
- package/dist/dtos/user.d.ts +2 -0
- package/dist/dtos/user.js +2 -0
- package/dist/dtos/user.js.map +1 -1
- package/dist/index.cjs +235 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -2
- package/dist/index.d.ts +17 -2
- package/dist/index.js +232 -10
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +62 -6
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.d.cts +1 -1
- package/dist/middlewares/index.d.ts +1 -1
- package/dist/middlewares/index.js +62 -6
- package/dist/middlewares/index.js.map +1 -1
- package/dist/{session-Dc5-JNoW.d.cts → session-Dwlcgxww.d.cts} +4 -1
- package/dist/{session-Dc5-JNoW.d.ts → session-Dwlcgxww.d.ts} +4 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +7 -0
- package/dist/types.d.ts +7 -0
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -2,8 +2,8 @@ export { IMfaChallenge, ISession, IUser } from './types.cjs';
|
|
|
2
2
|
import { IFonderieModule, IReadinessProblem, IFonderieApp } from '@fonderie/core';
|
|
3
3
|
import { IStoreAdapter } from '@fonderie/store';
|
|
4
4
|
import { EventBus } from '@fonderie/events';
|
|
5
|
-
import { I as IAuthConfig } from './session-
|
|
6
|
-
export { A as AUTH_CONFIG_KEYS, a as AuthLimitedRoute, b as AuthMessageKey, c as IAuthRateLimitConfig, d as IAuthRuntimeConfig, e as IAuthSecrets, f as IDataExportContributor, M as MESSAGE_KEYS, g as buildAuthAccountLimiter, h as buildAuthIpLimiter, w as withSession } from './session-
|
|
5
|
+
import { I as IAuthConfig } from './session-Dwlcgxww.cjs';
|
|
6
|
+
export { A as AUTH_CONFIG_KEYS, a as AuthLimitedRoute, b as AuthMessageKey, c as IAuthRateLimitConfig, d as IAuthRuntimeConfig, e as IAuthSecrets, f as IDataExportContributor, M as MESSAGE_KEYS, g as buildAuthAccountLimiter, h as buildAuthIpLimiter, w as withSession } from './session-Dwlcgxww.cjs';
|
|
7
7
|
export { IUserDTO, toUserDTO } from './dtos/user.cjs';
|
|
8
8
|
export { requireAuth, validate } from '@fonderie/core/middlewares';
|
|
9
9
|
import { z } from 'zod';
|
|
@@ -63,6 +63,21 @@ declare const DEFAULT_TEMPLATES: {
|
|
|
63
63
|
html: string;
|
|
64
64
|
text: string;
|
|
65
65
|
};
|
|
66
|
+
"oauth-registration": {
|
|
67
|
+
subject: string;
|
|
68
|
+
html: string;
|
|
69
|
+
text: string;
|
|
70
|
+
};
|
|
71
|
+
"oauth-linked": {
|
|
72
|
+
subject: string;
|
|
73
|
+
html: string;
|
|
74
|
+
text: string;
|
|
75
|
+
};
|
|
76
|
+
"oauth-unlinked": {
|
|
77
|
+
subject: string;
|
|
78
|
+
html: string;
|
|
79
|
+
text: string;
|
|
80
|
+
};
|
|
66
81
|
};
|
|
67
82
|
|
|
68
83
|
interface ILoginEventDTO {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ export { IMfaChallenge, ISession, IUser } from './types.js';
|
|
|
2
2
|
import { IFonderieModule, IReadinessProblem, IFonderieApp } from '@fonderie/core';
|
|
3
3
|
import { IStoreAdapter } from '@fonderie/store';
|
|
4
4
|
import { EventBus } from '@fonderie/events';
|
|
5
|
-
import { I as IAuthConfig } from './session-
|
|
6
|
-
export { A as AUTH_CONFIG_KEYS, a as AuthLimitedRoute, b as AuthMessageKey, c as IAuthRateLimitConfig, d as IAuthRuntimeConfig, e as IAuthSecrets, f as IDataExportContributor, M as MESSAGE_KEYS, g as buildAuthAccountLimiter, h as buildAuthIpLimiter, w as withSession } from './session-
|
|
5
|
+
import { I as IAuthConfig } from './session-Dwlcgxww.js';
|
|
6
|
+
export { A as AUTH_CONFIG_KEYS, a as AuthLimitedRoute, b as AuthMessageKey, c as IAuthRateLimitConfig, d as IAuthRuntimeConfig, e as IAuthSecrets, f as IDataExportContributor, M as MESSAGE_KEYS, g as buildAuthAccountLimiter, h as buildAuthIpLimiter, w as withSession } from './session-Dwlcgxww.js';
|
|
7
7
|
export { IUserDTO, toUserDTO } from './dtos/user.js';
|
|
8
8
|
export { requireAuth, validate } from '@fonderie/core/middlewares';
|
|
9
9
|
import { z } from 'zod';
|
|
@@ -63,6 +63,21 @@ declare const DEFAULT_TEMPLATES: {
|
|
|
63
63
|
html: string;
|
|
64
64
|
text: string;
|
|
65
65
|
};
|
|
66
|
+
"oauth-registration": {
|
|
67
|
+
subject: string;
|
|
68
|
+
html: string;
|
|
69
|
+
text: string;
|
|
70
|
+
};
|
|
71
|
+
"oauth-linked": {
|
|
72
|
+
subject: string;
|
|
73
|
+
html: string;
|
|
74
|
+
text: string;
|
|
75
|
+
};
|
|
76
|
+
"oauth-unlinked": {
|
|
77
|
+
subject: string;
|
|
78
|
+
html: string;
|
|
79
|
+
text: string;
|
|
80
|
+
};
|
|
66
81
|
};
|
|
67
82
|
|
|
68
83
|
interface ILoginEventDTO {
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,7 @@ var init_password = __esm({
|
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
// src/routes.ts
|
|
47
|
+
import { setApiResponse as setApiResponse6, HTTP as HTTP6 } from "@fonderie/core";
|
|
47
48
|
import { requireAuth, requireAnyAuth, requireVerified } from "@fonderie/core/middlewares";
|
|
48
49
|
|
|
49
50
|
// src/middlewares/require-email-login.ts
|
|
@@ -255,7 +256,16 @@ var MESSAGE_KEYS = {
|
|
|
255
256
|
mfaDisabled: "mfa-disabled",
|
|
256
257
|
mfaBackupCodesRegenerated: "mfa-backup-codes-regenerated",
|
|
257
258
|
emailChanged: "email-changed",
|
|
258
|
-
phoneChanged: "phone-changed"
|
|
259
|
+
phoneChanged: "phone-changed",
|
|
260
|
+
// Changing HOW an account can be signed into is a security event for the
|
|
261
|
+
// account's owner, who may not be the person doing it. Both directions are
|
|
262
|
+
// notified for the same reason password changes are.
|
|
263
|
+
// Welcome for an account CREATED by an OAuth sign-in. Distinct from
|
|
264
|
+
// emailRegistration, which carries a verification PIN — an OAuth signup
|
|
265
|
+
// arrives already verified by the provider and has nothing to confirm.
|
|
266
|
+
oauthRegistration: "oauth-registration",
|
|
267
|
+
oauthLinked: "oauth-linked",
|
|
268
|
+
oauthUnlinked: "oauth-unlinked"
|
|
259
269
|
};
|
|
260
270
|
var EVENT_KEYS = {
|
|
261
271
|
userRegistered: "fonderie.user.registered",
|
|
@@ -487,6 +497,8 @@ function toUserDTO(user, phoneVerified = false) {
|
|
|
487
497
|
isEmailVerified: user.emailVerifiedAt !== null,
|
|
488
498
|
isPhoneVerified: phoneVerified,
|
|
489
499
|
mfaEnabled: booleanOrFalse(user.mfaEnabled),
|
|
500
|
+
provider: stringOrEmpty(user.provider),
|
|
501
|
+
hasPassword: user.passwordHash !== null && user.passwordHash !== void 0,
|
|
490
502
|
suspended: booleanOrFalse(user.suspended),
|
|
491
503
|
whitelist: booleanOrFalse(user.whitelist),
|
|
492
504
|
ipWhitelist: Array.isArray(user.ipWhitelist) ? user.ipWhitelist : [],
|
|
@@ -514,6 +526,7 @@ var USER_COLUMNS = `
|
|
|
514
526
|
ip_whitelist AS "ipWhitelist",
|
|
515
527
|
mfa_enabled AS "mfaEnabled",
|
|
516
528
|
email_verified_at AS "emailVerifiedAt",
|
|
529
|
+
provider,
|
|
517
530
|
deleted_at AS "deletedAt",
|
|
518
531
|
created_at AS "createdAt",
|
|
519
532
|
updated_at AS "updatedAt"
|
|
@@ -591,6 +604,31 @@ var UserModel = class {
|
|
|
591
604
|
);
|
|
592
605
|
return row ?? null;
|
|
593
606
|
}
|
|
607
|
+
/**
|
|
608
|
+
* Clear the OAuth provider, but ONLY when the account can still be signed
|
|
609
|
+
* into afterwards.
|
|
610
|
+
*
|
|
611
|
+
* An account created through Google has no password. Unlinking it without
|
|
612
|
+
* checking would not be "removing a sign-in method" — it would be locking
|
|
613
|
+
* the owner out permanently, which is account deletion wearing a friendlier
|
|
614
|
+
* label. The guard lives in the WHERE clause so the check and the write are
|
|
615
|
+
* one atomic statement: a password cannot be removed between them.
|
|
616
|
+
*
|
|
617
|
+
* Returns false when the account has no password (nothing was changed) so
|
|
618
|
+
* the caller can say why rather than reporting a silent success.
|
|
619
|
+
*/
|
|
620
|
+
async clearProvider(id) {
|
|
621
|
+
const rows = await this.store.query(
|
|
622
|
+
`UPDATE fonderie_users
|
|
623
|
+
SET provider = NULL, provider_id = NULL
|
|
624
|
+
WHERE id = $1
|
|
625
|
+
AND password_hash IS NOT NULL
|
|
626
|
+
AND provider IS NOT NULL
|
|
627
|
+
RETURNING id`,
|
|
628
|
+
[id]
|
|
629
|
+
);
|
|
630
|
+
return rows.length > 0;
|
|
631
|
+
}
|
|
594
632
|
async updatePassword(id, passwordHash) {
|
|
595
633
|
await this.store.query(`UPDATE fonderie_users SET password_hash = $1 WHERE id = $2`, [
|
|
596
634
|
passwordHash,
|
|
@@ -691,14 +729,44 @@ var UserModel = class {
|
|
|
691
729
|
);
|
|
692
730
|
return row?.mfa_secret ?? null;
|
|
693
731
|
}
|
|
732
|
+
/**
|
|
733
|
+
* Link an OAuth identity to the account owning `email`, creating it if there
|
|
734
|
+
* is none.
|
|
735
|
+
*
|
|
736
|
+
* Returns what HAPPENED, not just who it happened to. Three outcomes hide
|
|
737
|
+
* behind one upsert, and callers must tell them apart:
|
|
738
|
+
*
|
|
739
|
+
* • `inserted` — a brand-new account. Whoever provisions on signup (the
|
|
740
|
+
* personal workspace, a wallet, a welcome email) must run, exactly as
|
|
741
|
+
* it does for a password registration. This is the case that was
|
|
742
|
+
* previously invisible: the upsert returned an id and looked identical
|
|
743
|
+
* to a returning user signing in, so OAuth signups silently skipped
|
|
744
|
+
* every signup side effect.
|
|
745
|
+
* • `previousProvider` differs — an existing account gained, or switched,
|
|
746
|
+
* a way to sign in. A security event for the owner.
|
|
747
|
+
* • `previousProvider` is the same — an ordinary login. Nothing to do, and
|
|
748
|
+
* notifying here would email the user on every single sign-in.
|
|
749
|
+
*
|
|
750
|
+
* The prior state is read in a CTE of the SAME statement, so it sees the
|
|
751
|
+
* snapshot from before the insert. Reading it in a separate query would
|
|
752
|
+
* race two concurrent logins into both seeing "new".
|
|
753
|
+
*/
|
|
694
754
|
async upsertByProvider(email2, provider, providerId) {
|
|
695
755
|
const [row] = await this.store.query(
|
|
696
|
-
`
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
756
|
+
`WITH prior AS (
|
|
757
|
+
SELECT provider FROM fonderie_users WHERE email = $1
|
|
758
|
+
), upserted AS (
|
|
759
|
+
INSERT INTO fonderie_users (email, email_verified_at, provider, provider_id)
|
|
760
|
+
VALUES ($1, now(), $2, $3)
|
|
761
|
+
ON CONFLICT (email) DO UPDATE
|
|
762
|
+
SET provider = $2, provider_id = $3,
|
|
763
|
+
email_verified_at = COALESCE(fonderie_users.email_verified_at, now())
|
|
764
|
+
RETURNING id
|
|
765
|
+
)
|
|
766
|
+
SELECT upserted.id,
|
|
767
|
+
NOT EXISTS (SELECT 1 FROM prior) AS inserted,
|
|
768
|
+
(SELECT provider FROM prior) AS "previousProvider"
|
|
769
|
+
FROM upserted`,
|
|
702
770
|
[email2, provider, providerId]
|
|
703
771
|
);
|
|
704
772
|
return row ?? null;
|
|
@@ -2191,6 +2259,50 @@ function userController(store, config, bus) {
|
|
|
2191
2259
|
await sessions.deleteByUser(ctx.user.id);
|
|
2192
2260
|
return setApiResponse4(HTTP4.OK, "PASSWORD_CHANGED", "Password updated successfully.");
|
|
2193
2261
|
},
|
|
2262
|
+
/**
|
|
2263
|
+
* Remove an OAuth provider from the account.
|
|
2264
|
+
*
|
|
2265
|
+
* Refuses when the account has no password, because unlinking would
|
|
2266
|
+
* then leave NO way to sign in — that is account deletion, not a
|
|
2267
|
+
* settings toggle, and a user clicking "disconnect Google" is not
|
|
2268
|
+
* asking for it. The check is inside the UPDATE's WHERE clause, so a
|
|
2269
|
+
* password cannot disappear between the check and the write.
|
|
2270
|
+
*
|
|
2271
|
+
* Notifies the account's email either way: changing how an account can
|
|
2272
|
+
* be signed into is a security event for its owner, who is not
|
|
2273
|
+
* necessarily the person doing it.
|
|
2274
|
+
*/
|
|
2275
|
+
unlinkOauth: async (ctx) => {
|
|
2276
|
+
const provider = String(ctx.meta["params"]?.["provider"] ?? "");
|
|
2277
|
+
const current = await users.findById(ctx.user.id);
|
|
2278
|
+
if (!current?.provider) {
|
|
2279
|
+
return setApiResponse4(HTTP4.NOT_FOUND, "NOT_LINKED", "No sign-in provider is linked.");
|
|
2280
|
+
}
|
|
2281
|
+
if (provider && provider.toLowerCase() !== current.provider.toLowerCase()) {
|
|
2282
|
+
return setApiResponse4(
|
|
2283
|
+
HTTP4.NOT_FOUND,
|
|
2284
|
+
"NOT_LINKED",
|
|
2285
|
+
`This account is not linked to ${provider}.`
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
const removed = await users.clearProvider(ctx.user.id);
|
|
2289
|
+
if (!removed) {
|
|
2290
|
+
return setApiResponse4(
|
|
2291
|
+
HTTP4.CONFLICT,
|
|
2292
|
+
"PASSWORD_REQUIRED",
|
|
2293
|
+
"Set a password before disconnecting this provider \u2014 it is currently the only way to sign in."
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
if (ctx.user.email) {
|
|
2297
|
+
await background4(bus?.emit(NOTIFICATION_EVENT3, {
|
|
2298
|
+
type: MESSAGE_KEYS.oauthUnlinked,
|
|
2299
|
+
locale: ctx.user.locale,
|
|
2300
|
+
data: { provider: current.provider },
|
|
2301
|
+
recipient: { email: ctx.user.email, phone: null, deviceToken: null }
|
|
2302
|
+
}));
|
|
2303
|
+
}
|
|
2304
|
+
return setApiResponse4(HTTP4.OK, "OAUTH_UNLINKED", "Sign-in provider removed.");
|
|
2305
|
+
},
|
|
2194
2306
|
deleteMe: async (ctx) => {
|
|
2195
2307
|
const userId = ctx.user.id;
|
|
2196
2308
|
await users.softDelete(userId);
|
|
@@ -2250,7 +2362,8 @@ function userController(store, config, bus) {
|
|
|
2250
2362
|
// src/controllers/oauth.controller.ts
|
|
2251
2363
|
import { randomBytes as randomBytes4, createPublicKey, createHash as createHash2 } from "crypto";
|
|
2252
2364
|
import jwt2 from "jsonwebtoken";
|
|
2253
|
-
import { setApiResponse as setApiResponse5, HTTP as HTTP5, constantTimeEqual as constantTimeEqual2 } from "@fonderie/core";
|
|
2365
|
+
import { setApiResponse as setApiResponse5, HTTP as HTTP5, constantTimeEqual as constantTimeEqual2, background as background5 } from "@fonderie/core";
|
|
2366
|
+
import { NOTIFICATION_EVENT as NOTIFICATION_EVENT4 } from "@fonderie/events";
|
|
2254
2367
|
|
|
2255
2368
|
// src/models/consumed-token.model.ts
|
|
2256
2369
|
var ConsumedTokenModel = class {
|
|
@@ -2351,11 +2464,56 @@ async function verifyAppleIdToken(idToken, opts) {
|
|
|
2351
2464
|
}
|
|
2352
2465
|
}
|
|
2353
2466
|
var appleEmailVerified = (c) => c.email_verified === true || c.email_verified === "true";
|
|
2354
|
-
function oauthController(store, config) {
|
|
2467
|
+
function oauthController(store, config, bus) {
|
|
2355
2468
|
const users = new UserModel(store);
|
|
2356
2469
|
const sessions = new SessionModel(store);
|
|
2357
2470
|
const loginEvents = new LoginEventModel(store);
|
|
2358
2471
|
const consumedTokens = new ConsumedTokenModel(store);
|
|
2472
|
+
const announceOAuthUpsert = async (ctx, upserted, provider, user) => {
|
|
2473
|
+
if (!bus) return;
|
|
2474
|
+
const reqId = ctx.meta["requestId"];
|
|
2475
|
+
const reqOpts = reqId !== void 0 ? { requestId: reqId } : void 0;
|
|
2476
|
+
const label = provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
2477
|
+
const recipient = { email: user.email, phone: null, deviceToken: null };
|
|
2478
|
+
if (upserted.inserted) {
|
|
2479
|
+
await background5(bus.emit(
|
|
2480
|
+
EVENT_KEYS.userRegistered,
|
|
2481
|
+
{
|
|
2482
|
+
userId: upserted.id,
|
|
2483
|
+
email: user.email,
|
|
2484
|
+
firstName: user.firstName,
|
|
2485
|
+
lastName: user.lastName,
|
|
2486
|
+
loginMethod: provider
|
|
2487
|
+
},
|
|
2488
|
+
reqOpts
|
|
2489
|
+
));
|
|
2490
|
+
if (user.email) {
|
|
2491
|
+
await background5(bus.emit(
|
|
2492
|
+
NOTIFICATION_EVENT4,
|
|
2493
|
+
{
|
|
2494
|
+
type: MESSAGE_KEYS.oauthRegistration,
|
|
2495
|
+
locale: user.locale,
|
|
2496
|
+
data: { provider: label, appName: config.appName ?? "Fonderie" },
|
|
2497
|
+
recipient
|
|
2498
|
+
},
|
|
2499
|
+
reqOpts
|
|
2500
|
+
));
|
|
2501
|
+
}
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2504
|
+
if (upserted.previousProvider !== provider && user.email) {
|
|
2505
|
+
await background5(bus.emit(
|
|
2506
|
+
NOTIFICATION_EVENT4,
|
|
2507
|
+
{
|
|
2508
|
+
type: MESSAGE_KEYS.oauthLinked,
|
|
2509
|
+
locale: user.locale,
|
|
2510
|
+
data: { provider: label },
|
|
2511
|
+
recipient
|
|
2512
|
+
},
|
|
2513
|
+
reqOpts
|
|
2514
|
+
));
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2359
2517
|
const completeAppleLogin = async (ctx, claims) => {
|
|
2360
2518
|
const meta = requestMeta(ctx);
|
|
2361
2519
|
if (!claims.email) {
|
|
@@ -2384,6 +2542,7 @@ function oauthController(store, config) {
|
|
|
2384
2542
|
if (!fullUser) {
|
|
2385
2543
|
return setApiResponse5(HTTP5.SERVER_ERROR, "SERVER_ERROR", "Apple login failed");
|
|
2386
2544
|
}
|
|
2545
|
+
await announceOAuthUpsert(ctx, upserted, "apple", fullUser);
|
|
2387
2546
|
const { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, config, { loginMethod: "apple" });
|
|
2388
2547
|
await sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);
|
|
2389
2548
|
await loginEvents.recordSafe({
|
|
@@ -2534,6 +2693,7 @@ function oauthController(store, config) {
|
|
|
2534
2693
|
if (!fullUser) {
|
|
2535
2694
|
return setApiResponse5(HTTP5.SERVER_ERROR, "SERVER_ERROR", "OAuth login failed");
|
|
2536
2695
|
}
|
|
2696
|
+
await announceOAuthUpsert(ctx, upserted, "google", fullUser);
|
|
2537
2697
|
const { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, config, {
|
|
2538
2698
|
loginMethod: "google"
|
|
2539
2699
|
});
|
|
@@ -2698,7 +2858,7 @@ function oauthController(store, config) {
|
|
|
2698
2858
|
function buildAuthRoutes(store, config, bus) {
|
|
2699
2859
|
const user = userController(store, config, bus);
|
|
2700
2860
|
const auth = authController(store, config, bus);
|
|
2701
|
-
const oauth = oauthController(store, config);
|
|
2861
|
+
const oauth = oauthController(store, config, bus);
|
|
2702
2862
|
const mfa = mfaController(store, config, config.appName ?? "Fonderie", bus);
|
|
2703
2863
|
const verifyGate = config.requireVerification ? requireVerified : (_ctx, next) => next();
|
|
2704
2864
|
const passthrough = (_ctx, next) => next();
|
|
@@ -2711,6 +2871,27 @@ function buildAuthRoutes(store, config, bus) {
|
|
|
2711
2871
|
return [o.method ?? method, o.path ?? path, ...handlers];
|
|
2712
2872
|
};
|
|
2713
2873
|
const routes = [
|
|
2874
|
+
// Which sign-in methods this deployment can actually honour (Public).
|
|
2875
|
+
//
|
|
2876
|
+
// A frontend has to decide which buttons to draw, and the only honest
|
|
2877
|
+
// source is the side holding the credentials. The alternative — a
|
|
2878
|
+
// build-time flag in the app — stores the same fact twice and lets the
|
|
2879
|
+
// two disagree; the symptom is a user clicking a provider the server
|
|
2880
|
+
// cannot complete and landing on the provider's error page, which the
|
|
2881
|
+
// app cannot explain.
|
|
2882
|
+
//
|
|
2883
|
+
// Public and unauthenticated on purpose: the login screen needs it
|
|
2884
|
+
// before anyone has signed in. It discloses nothing a visitor could not
|
|
2885
|
+
// learn by looking at the buttons, and deliberately nothing else — no
|
|
2886
|
+
// client ids, no redirect URIs, no module inventory.
|
|
2887
|
+
R(
|
|
2888
|
+
"providers",
|
|
2889
|
+
"GET",
|
|
2890
|
+
"/auth/providers",
|
|
2891
|
+
async () => setApiResponse6(HTTP6.OK, "AUTH_PROVIDERS", "Sign-in methods available here", {
|
|
2892
|
+
providers: [...config.providers]
|
|
2893
|
+
})
|
|
2894
|
+
),
|
|
2714
2895
|
// Registration & Login (Public)
|
|
2715
2896
|
R("register", "POST", "/auth/register", ipLimit("register"), validate(registerSchema), auth.register),
|
|
2716
2897
|
R("login", "POST", "/auth/login", ipLimit("login"), validate(loginSchema), acctLimit("login"), auth.login),
|
|
@@ -2735,6 +2916,9 @@ function buildAuthRoutes(store, config, bus) {
|
|
|
2735
2916
|
R("updateEmail", "PUT", "/users/email", requireAuth, verifyGate, validate(updateEmailSchema), user.updateEmail),
|
|
2736
2917
|
R("updatePhone", "PUT", "/users/phone", requireAuth, verifyGate, validate(updatePhoneSchema), user.updatePhone),
|
|
2737
2918
|
R("changePassword", "PUT", "/users/password", requireAuth, validate(changePasswordSchema), user.changePassword),
|
|
2919
|
+
// Disconnect an OAuth provider. requireAuth only — the controller
|
|
2920
|
+
// refuses when it would leave the account with no way to sign in.
|
|
2921
|
+
R("unlinkOauth", "DELETE", "/auth/oauth/:provider", requireAuth, user.unlinkOauth),
|
|
2738
2922
|
R("deleteMe", "DELETE", "/users", requireAuth, verifyGate, user.deleteMe),
|
|
2739
2923
|
R("exportMe", "GET", "/users/export", requireAuth, user.exportMe),
|
|
2740
2924
|
// Security surfaces (Protected; the caller's own login history + sessions).
|
|
@@ -3047,9 +3231,47 @@ If you made this change, you're all set. If not, contact support right away \u20
|
|
|
3047
3231
|
The phone number on your account was just updated.
|
|
3048
3232
|
|
|
3049
3233
|
If you made this change, you're all set. If not, contact support right away \u2014 someone may have access to your account.`
|
|
3234
|
+
},
|
|
3235
|
+
[MESSAGE_KEYS.oauthRegistration]: {
|
|
3236
|
+
subject: "Welcome to {{appName}}",
|
|
3237
|
+
html: `<h1>Welcome to {{appName}}</h1>
|
|
3238
|
+
<p>Your account was created using <strong>{{provider}}</strong>. Sign in any time with the same {{provider}} account — there is no password to remember.</p>
|
|
3239
|
+
<p class="muted">If you did not create this account, contact support.</p>`,
|
|
3240
|
+
text: `Welcome
|
|
3241
|
+
|
|
3242
|
+
Your account was created using {{provider}}. Sign in any time with the same
|
|
3243
|
+
{{provider}} account \u2014 there is no password to remember.
|
|
3244
|
+
|
|
3245
|
+
If you did not create this account, contact support.`
|
|
3246
|
+
},
|
|
3247
|
+
[MESSAGE_KEYS.oauthLinked]: {
|
|
3248
|
+
subject: "A new sign-in method was added to your account",
|
|
3249
|
+
html: `<h1>{{provider}} sign-in was added</h1>
|
|
3250
|
+
<p>Your account can now also be signed into with <strong>{{provider}}</strong>.</p>
|
|
3251
|
+
<p class="muted">If you did this, nothing more is needed. If not, contact support right away and change your password — someone else may be able to sign in as you.</p>`,
|
|
3252
|
+
text: `{{provider}} sign-in was added
|
|
3253
|
+
|
|
3254
|
+
Your account can now also be signed into with {{provider}}.
|
|
3255
|
+
|
|
3256
|
+
If you did this, nothing more is needed. If not, contact support right away and
|
|
3257
|
+
change your password \u2014 someone else may be able to sign in as you.`
|
|
3258
|
+
},
|
|
3259
|
+
[MESSAGE_KEYS.oauthUnlinked]: {
|
|
3260
|
+
subject: "A sign-in method was removed from your account",
|
|
3261
|
+
html: `<h1>{{provider}} sign-in was removed</h1>
|
|
3262
|
+
<p><strong>{{provider}}</strong> can no longer be used to sign in. Your email and password still work.</p>
|
|
3263
|
+
<p class="muted">If you did this, you’re all set. If not, contact support right away.</p>`,
|
|
3264
|
+
text: `{{provider}} sign-in was removed
|
|
3265
|
+
|
|
3266
|
+
{{provider}} can no longer be used to sign in. Your email and password still work.
|
|
3267
|
+
|
|
3268
|
+
If you did this, you're all set. If not, contact support right away.`
|
|
3050
3269
|
}
|
|
3051
3270
|
};
|
|
3052
3271
|
var SAMPLE_PAYLOADS = {
|
|
3272
|
+
[MESSAGE_KEYS.oauthRegistration]: { provider: "Google", appName: "Fonderie" },
|
|
3273
|
+
[MESSAGE_KEYS.oauthLinked]: { provider: "Google" },
|
|
3274
|
+
[MESSAGE_KEYS.oauthUnlinked]: { provider: "Google" },
|
|
3053
3275
|
[MESSAGE_KEYS.emailRegistration]: { firstName: "", pin: "123456" },
|
|
3054
3276
|
[MESSAGE_KEYS.emailVerification]: { firstName: "Ada", pin: "123456" },
|
|
3055
3277
|
[MESSAGE_KEYS.passwordReset]: {
|