@luckystack/login 0.14.0 → 0.16.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/CHANGELOG.md +51 -0
- package/README.md +6 -0
- package/dist/{chunk-CLYMWF3A.js → chunk-CYGFR56I.js} +51 -9
- package/dist/chunk-CYGFR56I.js.map +1 -0
- package/dist/{chunk-6IGNAMXW.js → chunk-MZNCQ26U.js} +32 -6
- package/dist/chunk-MZNCQ26U.js.map +1 -0
- package/dist/emailChange.d.ts +6 -0
- package/dist/forgotPassword.d.ts +3 -2
- package/dist/hookPayloads.d.ts +5 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +204 -61
- package/dist/index.js.map +1 -1
- package/dist/logout-VTVJTFSZ.js +7 -0
- package/dist/oauthProviders.d.ts +48 -4
- package/dist/passwordReset.d.ts +9 -2
- package/dist/register.js +1 -1
- package/dist/session.d.ts +5 -3
- package/dist/sessionAdapter.d.ts +9 -0
- package/dist/userAdapter.d.ts +22 -0
- package/docs/oauth-providers.md +4 -0
- package/docs/password-reset.md +1 -1
- package/docs/user-adapter.md +2 -0
- package/package.json +3 -3
- package/dist/chunk-6IGNAMXW.js.map +0 -1
- package/dist/chunk-CLYMWF3A.js.map +0 -1
- package/dist/logout-RD727I3V.js +0 -7
- /package/dist/{logout-RD727I3V.js.map → logout-VTVJTFSZ.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
registerOAuthProviderFactory,
|
|
15
15
|
registerOAuthProviders,
|
|
16
16
|
registerUserAdapter
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-CYGFR56I.js";
|
|
18
18
|
import {
|
|
19
19
|
activeUsersKeyFor,
|
|
20
20
|
deleteSession,
|
|
@@ -29,10 +29,10 @@ import {
|
|
|
29
29
|
revokeUserSessions,
|
|
30
30
|
saveSession,
|
|
31
31
|
sessionKeyFor
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-MZNCQ26U.js";
|
|
33
33
|
|
|
34
34
|
// src/index.ts
|
|
35
|
-
import { registerSessionProvider } from "@luckystack/core";
|
|
35
|
+
import { registerRedactedLogKeys, registerSessionProvider } from "@luckystack/core";
|
|
36
36
|
|
|
37
37
|
// src/authLockout.ts
|
|
38
38
|
import {
|
|
@@ -515,6 +515,17 @@ var readStringField = (record, key, fallback = "") => {
|
|
|
515
515
|
if (typeof value === "boolean") return String(value);
|
|
516
516
|
return fallback;
|
|
517
517
|
};
|
|
518
|
+
var readAccountId = (record, key) => {
|
|
519
|
+
const value = record[key];
|
|
520
|
+
if (typeof value === "string" && value !== "") return value;
|
|
521
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) return String(value);
|
|
522
|
+
return void 0;
|
|
523
|
+
};
|
|
524
|
+
var compareStoredAccountId = (stored, accountId) => {
|
|
525
|
+
if (stored === null || stored === void 0 || stored === "") return "unbound";
|
|
526
|
+
const comparable = typeof stored === "string" || typeof stored === "number" || typeof stored === "bigint";
|
|
527
|
+
return comparable && String(stored) === accountId ? "match" : "mismatch";
|
|
528
|
+
};
|
|
518
529
|
var generateAvatarFallbackColor = () => `#${Math.floor(Math.random() * 16777215).toString(16).padStart(6, "0")}`;
|
|
519
530
|
var resolveUploadedAvatar = async (userId) => {
|
|
520
531
|
const filePath = path.join(uploadsFolder(), `${userId}.webp`);
|
|
@@ -522,7 +533,13 @@ var resolveUploadedAvatar = async (userId) => {
|
|
|
522
533
|
return avatarStat ? `${userId}.webp` : null;
|
|
523
534
|
};
|
|
524
535
|
var sanitizeUserForSession = (user) => {
|
|
525
|
-
const {
|
|
536
|
+
const {
|
|
537
|
+
password: _password,
|
|
538
|
+
totpSecret: _totpSecret,
|
|
539
|
+
recoveryCodes: _recoveryCodes,
|
|
540
|
+
providerAccountId: _providerAccountId,
|
|
541
|
+
...safeUser
|
|
542
|
+
} = user;
|
|
526
543
|
return safeUser;
|
|
527
544
|
};
|
|
528
545
|
var emitLoginFailed = (payload) => {
|
|
@@ -841,6 +858,7 @@ var exchangeOAuthToken = async (provider, code, codeVerifier) => {
|
|
|
841
858
|
}
|
|
842
859
|
return accessToken;
|
|
843
860
|
};
|
|
861
|
+
var refuseOAuth = (refusal = "oauth.userResolveFailed", userId) => userId === void 0 ? { refusal } : { refusal, userId };
|
|
844
862
|
var fetchOAuthProfile = async (provider, accessToken) => {
|
|
845
863
|
const getUserData = async () => {
|
|
846
864
|
const response2 = await fetch(provider.userInfoURL, {
|
|
@@ -863,9 +881,20 @@ var fetchOAuthProfile = async (provider, accessToken) => {
|
|
|
863
881
|
return null;
|
|
864
882
|
}
|
|
865
883
|
const userData = asRecord(response);
|
|
884
|
+
let accountId;
|
|
885
|
+
if (provider.accountIdKey) {
|
|
886
|
+
accountId = readAccountId(userData, provider.accountIdKey);
|
|
887
|
+
if (accountId === void 0) {
|
|
888
|
+
getLogger3().warn("oauth: provider userinfo has no stable account id \u2014 refusing the login", {
|
|
889
|
+
provider: provider.name,
|
|
890
|
+
accountIdKey: provider.accountIdKey
|
|
891
|
+
});
|
|
892
|
+
return refuseOAuth("oauth.accountIdMissing");
|
|
893
|
+
}
|
|
894
|
+
}
|
|
866
895
|
const resolvedName = readStringField(userData, provider.nameKey);
|
|
867
896
|
const name = resolvedName === "" ? "didnt find a name" : resolvedName;
|
|
868
|
-
const emailValue = userData[provider.emailKey];
|
|
897
|
+
const emailValue = provider.alwaysUseGetEmail ? void 0 : userData[provider.emailKey];
|
|
869
898
|
let email = typeof emailValue === "string" ? emailValue : void 0;
|
|
870
899
|
let emailVerified = false;
|
|
871
900
|
if (email && provider.emailVerifiedKey) {
|
|
@@ -883,13 +912,14 @@ var fetchOAuthProfile = async (provider, accessToken) => {
|
|
|
883
912
|
const avatarValue = provider.avatarKey ? readStringField(userData, provider.avatarKey) : provider.getAvatar ? await provider.getAvatar({ userData, avatarId: typeof avatarId === "string" ? avatarId : "", accessToken }) : "";
|
|
884
913
|
const avatar = typeof avatarValue === "string" ? avatarValue : "";
|
|
885
914
|
if (!email && provider.getEmail) {
|
|
886
|
-
const
|
|
915
|
+
const selected = await provider.getEmail(accessToken, userData);
|
|
916
|
+
const selectedEmail = typeof selected === "object" ? selected.email : selected;
|
|
887
917
|
if (!selectedEmail) {
|
|
888
918
|
getLogger3().warn("oauth: no email found via provider.getEmail", { provider: provider.name });
|
|
889
919
|
return null;
|
|
890
920
|
}
|
|
891
921
|
email = selectedEmail;
|
|
892
|
-
emailVerified = true;
|
|
922
|
+
emailVerified = typeof selected === "object" ? selected.verified : true;
|
|
893
923
|
}
|
|
894
924
|
if (email !== void 0) {
|
|
895
925
|
const normalizedEmail = email.trim().toLowerCase();
|
|
@@ -899,23 +929,75 @@ var fetchOAuthProfile = async (provider, accessToken) => {
|
|
|
899
929
|
}
|
|
900
930
|
email = normalizedEmail;
|
|
901
931
|
}
|
|
902
|
-
return { email, name, avatar, emailVerified, rawUserData: userData };
|
|
932
|
+
return { email, name, avatar, emailVerified, accountId, rawUserData: userData };
|
|
903
933
|
};
|
|
904
|
-
var
|
|
905
|
-
|
|
906
|
-
if (
|
|
907
|
-
|
|
934
|
+
var warnedMissingAccountIdLookup = false;
|
|
935
|
+
var supportsAccountBinding = (adapter) => {
|
|
936
|
+
if (typeof adapter.findByProviderAccountId === "function") return true;
|
|
937
|
+
if (!warnedMissingAccountIdLookup) {
|
|
938
|
+
warnedMissingAccountIdLookup = true;
|
|
939
|
+
getLogger3().warn(
|
|
940
|
+
"[login] the registered UserAdapter does not implement findByProviderAccountId \u2014 OAuth accounts are not bound to the provider id: existing accounts are still matched by verified email only, and an email change at the provider starts a new account. Implement findByProviderAccountId and store providerAccountId to enable binding."
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
return false;
|
|
944
|
+
};
|
|
945
|
+
var signInExistingOAuthUser = async (userAdapter, existing) => {
|
|
946
|
+
const uploadedAvatar = await resolveUploadedAvatar(existing.id);
|
|
947
|
+
if (uploadedAvatar) {
|
|
948
|
+
existing.avatar = uploadedAvatar;
|
|
949
|
+
}
|
|
950
|
+
const previousLogin = existing.lastLogin ?? null;
|
|
951
|
+
const nowLogin = /* @__PURE__ */ new Date();
|
|
952
|
+
await tryCatch2(() => userAdapter.update(existing.id, { lastLogin: nowLogin }));
|
|
953
|
+
return {
|
|
954
|
+
user: {
|
|
955
|
+
...sanitizeUserForSession(existing),
|
|
956
|
+
token: "",
|
|
957
|
+
lastLogin: nowLogin,
|
|
958
|
+
previousLogin
|
|
959
|
+
},
|
|
960
|
+
isNewUser: false
|
|
961
|
+
};
|
|
962
|
+
};
|
|
963
|
+
var passesOAuthPreLogin = async (provider, providerEmail, existing) => {
|
|
964
|
+
const storedEmail = typeof existing?.email === "string" && existing.email !== "" ? existing.email : void 0;
|
|
965
|
+
const preLoginResult = await dispatchHook("preLogin", {
|
|
966
|
+
email: storedEmail ?? providerEmail,
|
|
967
|
+
provider: provider.name,
|
|
968
|
+
...existing ? { userId: existing.id } : {},
|
|
969
|
+
providerEmail
|
|
970
|
+
});
|
|
908
971
|
if (preLoginResult.stopped) {
|
|
909
972
|
getLogger3().warn(`oauth login aborted by preLogin hook`, { errorCode: preLoginResult.signal.errorCode });
|
|
910
|
-
return
|
|
973
|
+
return false;
|
|
911
974
|
}
|
|
975
|
+
return true;
|
|
976
|
+
};
|
|
977
|
+
var findOrCreateOAuthUser = async (provider, profile) => {
|
|
978
|
+
const { email, name, avatar, emailVerified, accountId } = profile;
|
|
979
|
+
if (!email) return refuseOAuth();
|
|
912
980
|
const userAdapter = getUserAdapter();
|
|
981
|
+
const bindingId = accountId !== void 0 && supportsAccountBinding(userAdapter) ? accountId : void 0;
|
|
982
|
+
if (bindingId !== void 0) {
|
|
983
|
+
const [boundError, boundUser] = await tryCatch2(
|
|
984
|
+
async () => await userAdapter.findByProviderAccountId?.({ provider: provider.name, providerAccountId: bindingId }) ?? null
|
|
985
|
+
);
|
|
986
|
+
if (boundError) {
|
|
987
|
+
getLogger3().error("oauth: findByProviderAccountId failed", boundError);
|
|
988
|
+
return refuseOAuth();
|
|
989
|
+
}
|
|
990
|
+
if (boundUser?.id) {
|
|
991
|
+
if (!await passesOAuthPreLogin(provider, email, boundUser)) return refuseOAuth(void 0, boundUser.id);
|
|
992
|
+
return signInExistingOAuthUser(userAdapter, boundUser);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
913
995
|
const [findError, findResponse] = await tryCatch2(
|
|
914
996
|
() => resolveUserByEmail(userAdapter, { email, provider: provider.name })
|
|
915
997
|
);
|
|
916
998
|
if (findError) {
|
|
917
999
|
getLogger3().error("oauth: findByEmail failed", findError);
|
|
918
|
-
return
|
|
1000
|
+
return refuseOAuth();
|
|
919
1001
|
}
|
|
920
1002
|
if (findResponse?.id) {
|
|
921
1003
|
const existingProvider = findResponse.provider ?? null;
|
|
@@ -925,40 +1007,56 @@ var findOrCreateOAuthUser = async (provider, profile) => {
|
|
|
925
1007
|
"oauth: refusing to link an unverified provider email to an existing account created by a different provider (account-takeover guard)",
|
|
926
1008
|
{ provider: provider.name, existingProvider }
|
|
927
1009
|
);
|
|
928
|
-
return
|
|
1010
|
+
return refuseOAuth("oauth.emailUnverified", findResponse.id);
|
|
929
1011
|
}
|
|
930
|
-
|
|
931
|
-
if (
|
|
932
|
-
findResponse.
|
|
1012
|
+
let bindTo;
|
|
1013
|
+
if (accountId !== void 0 && (!isCrossProviderLink || existingProvider === null)) {
|
|
1014
|
+
const stored = compareStoredAccountId(findResponse.providerAccountId, accountId);
|
|
1015
|
+
if (stored === "mismatch") {
|
|
1016
|
+
getLogger3().warn(
|
|
1017
|
+
"oauth: refusing login \u2014 the account with this email is bound to a different provider account (account-takeover guard)",
|
|
1018
|
+
{ provider: provider.name, userId: findResponse.id }
|
|
1019
|
+
);
|
|
1020
|
+
return refuseOAuth("oauth.accountBoundElsewhere", findResponse.id);
|
|
1021
|
+
}
|
|
1022
|
+
if (stored === "unbound" && !isCrossProviderLink) {
|
|
1023
|
+
if (!emailVerified) {
|
|
1024
|
+
getLogger3().warn(
|
|
1025
|
+
"oauth: refusing login \u2014 the account with this email is not bound to a provider id yet and the provider did not verify the email (account-takeover guard)",
|
|
1026
|
+
{ provider: provider.name, userId: findResponse.id }
|
|
1027
|
+
);
|
|
1028
|
+
return refuseOAuth("oauth.emailUnverified", findResponse.id);
|
|
1029
|
+
}
|
|
1030
|
+
bindTo = bindingId;
|
|
1031
|
+
}
|
|
933
1032
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
isNewUser: false
|
|
945
|
-
};
|
|
1033
|
+
if (!await passesOAuthPreLogin(provider, email, findResponse)) return refuseOAuth(void 0, findResponse.id);
|
|
1034
|
+
if (bindTo !== void 0) {
|
|
1035
|
+
const providerAccountId = bindTo;
|
|
1036
|
+
const [bindError] = await tryCatch2(() => userAdapter.update(findResponse.id, { providerAccountId }));
|
|
1037
|
+
if (bindError) {
|
|
1038
|
+
getLogger3().error("oauth: binding the account to the provider id failed", bindError);
|
|
1039
|
+
return refuseOAuth(void 0, findResponse.id);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return signInExistingOAuthUser(userAdapter, findResponse);
|
|
946
1043
|
}
|
|
1044
|
+
if (!await passesOAuthPreLogin(provider, email)) return refuseOAuth();
|
|
947
1045
|
if (!emailVerified) {
|
|
948
1046
|
getLogger3().warn(
|
|
949
1047
|
"oauth: refusing first-login account creation for an unverified provider email (account-squatting guard)",
|
|
950
1048
|
{ provider: provider.name }
|
|
951
1049
|
);
|
|
952
|
-
return
|
|
1050
|
+
return refuseOAuth();
|
|
953
1051
|
}
|
|
954
1052
|
if (!getProjectConfig4().auth.allowRegistration) {
|
|
955
1053
|
getLogger3().warn("oauth: registration disabled \u2014 refusing first-login account creation");
|
|
956
|
-
return
|
|
1054
|
+
return refuseOAuth();
|
|
957
1055
|
}
|
|
958
1056
|
const preRegisterResult = await dispatchHook("preRegister", { email, provider: provider.name, name });
|
|
959
1057
|
if (preRegisterResult.stopped) {
|
|
960
1058
|
getLogger3().warn(`oauth register aborted by preRegister hook`, { errorCode: preRegisterResult.signal.errorCode });
|
|
961
|
-
return
|
|
1059
|
+
return refuseOAuth();
|
|
962
1060
|
}
|
|
963
1061
|
const createNewUser = async () => userAdapter.create({
|
|
964
1062
|
email,
|
|
@@ -966,14 +1064,15 @@ var findOrCreateOAuthUser = async (provider, profile) => {
|
|
|
966
1064
|
name,
|
|
967
1065
|
avatar,
|
|
968
1066
|
avatarFallback: generateAvatarFallbackColor(),
|
|
969
|
-
language: getProjectConfig4().defaultLanguage
|
|
1067
|
+
language: getProjectConfig4().defaultLanguage,
|
|
1068
|
+
...bindingId === void 0 ? {} : { providerAccountId: bindingId }
|
|
970
1069
|
});
|
|
971
1070
|
const [createError, createResponse] = await tryCatch2(createNewUser);
|
|
972
1071
|
if (createError) {
|
|
973
1072
|
getLogger3().error("oauth: user create failed", createError);
|
|
974
|
-
return
|
|
1073
|
+
return refuseOAuth();
|
|
975
1074
|
}
|
|
976
|
-
if (!createResponse) return
|
|
1075
|
+
if (!createResponse) return refuseOAuth();
|
|
977
1076
|
return {
|
|
978
1077
|
user: { ...sanitizeUserForSession(createResponse), token: "" },
|
|
979
1078
|
isNewUser: true
|
|
@@ -1075,9 +1174,19 @@ var loginCallback = async (pathname, req, _res, options = {}) => {
|
|
|
1075
1174
|
emitLoginFailed({ provider: providerName, reason: "oauth.profileFetchFailed", stage: "oauth" });
|
|
1076
1175
|
return false;
|
|
1077
1176
|
}
|
|
1177
|
+
if ("refusal" in profile) {
|
|
1178
|
+
emitLoginFailed({ provider: providerName, reason: profile.refusal, stage: "oauth" });
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1078
1181
|
const resolved = await findOrCreateOAuthUser(provider, profile);
|
|
1079
|
-
if (
|
|
1080
|
-
emitLoginFailed({
|
|
1182
|
+
if ("refusal" in resolved) {
|
|
1183
|
+
emitLoginFailed({
|
|
1184
|
+
provider: providerName,
|
|
1185
|
+
email: profile.email,
|
|
1186
|
+
...resolved.userId === void 0 ? {} : { userId: resolved.userId },
|
|
1187
|
+
reason: resolved.refusal,
|
|
1188
|
+
stage: "oauth"
|
|
1189
|
+
});
|
|
1081
1190
|
return false;
|
|
1082
1191
|
}
|
|
1083
1192
|
const newToken = randomBytes(32).toString("hex");
|
|
@@ -1137,6 +1246,14 @@ var createPasswordResetToken = async (userId) => {
|
|
|
1137
1246
|
var consumePasswordResetToken = async (token) => {
|
|
1138
1247
|
return consumeOneTimeToken(PWRESET_NAMESPACE, token);
|
|
1139
1248
|
};
|
|
1249
|
+
var invalidatePasswordResetToken = async (userId) => {
|
|
1250
|
+
const pointerKey = pwresetUserKey(userId);
|
|
1251
|
+
const priorKey = await redis.get(pointerKey);
|
|
1252
|
+
if (priorKey) {
|
|
1253
|
+
await redis.del(priorKey);
|
|
1254
|
+
}
|
|
1255
|
+
await redis.del(pointerKey);
|
|
1256
|
+
};
|
|
1140
1257
|
var updatePasswordHash = async (userId, plaintext) => {
|
|
1141
1258
|
const reason = validatePassword(plaintext);
|
|
1142
1259
|
if (reason) {
|
|
@@ -1196,28 +1313,34 @@ var sendPasswordResetEmail = async ({ email, brand }) => {
|
|
|
1196
1313
|
});
|
|
1197
1314
|
return { ok: true };
|
|
1198
1315
|
}
|
|
1199
|
-
const
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1316
|
+
const deliverResetEmail = async () => {
|
|
1317
|
+
const token = await createPasswordResetToken(user.id);
|
|
1318
|
+
void dispatchHook2("passwordResetRequested", {
|
|
1319
|
+
email,
|
|
1320
|
+
matched: true,
|
|
1321
|
+
userId: user.id,
|
|
1322
|
+
ttlSeconds: config.auth.passwordResetTtlSeconds
|
|
1323
|
+
});
|
|
1324
|
+
const baseUrl = (config.app.publicUrl || "").replace(/\/+$/, "");
|
|
1325
|
+
const resetPath = config.auth.passwordResetPath || "/reset-password";
|
|
1326
|
+
const resetUrl = `${baseUrl}${resetPath}?token=${encodeURIComponent(token)}`;
|
|
1327
|
+
const ttlMinutes = Math.round(config.auth.passwordResetTtlSeconds / 60);
|
|
1328
|
+
const result = await sendEmail({
|
|
1329
|
+
to: user.email,
|
|
1330
|
+
template: "password-reset",
|
|
1331
|
+
data: { resetUrl, userName: user.name, brand: resolvedBrand, ttlMinutes },
|
|
1332
|
+
adapterHint: "transactional"
|
|
1333
|
+
});
|
|
1334
|
+
if (result.ok) {
|
|
1335
|
+
getLogger4().info("[forgotPassword] reset email dispatched", { userId: user.id });
|
|
1336
|
+
} else {
|
|
1337
|
+
getLogger4().warn("[forgotPassword] reset email send FAILED", { userId: user.id, reason: result.reason });
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
void deliverResetEmail().catch((error) => {
|
|
1341
|
+
getLogger4().error("[forgotPassword] reset email delivery failed", { userId: user.id, error: String(error) });
|
|
1214
1342
|
});
|
|
1215
|
-
|
|
1216
|
-
getLogger4().info("[forgotPassword] reset email dispatched", { userId: user.id });
|
|
1217
|
-
} else {
|
|
1218
|
-
getLogger4().warn("[forgotPassword] reset email send FAILED", { userId: user.id, reason: result.reason });
|
|
1219
|
-
}
|
|
1220
|
-
return result.ok ? { ok: true } : { ok: false, reason: result.reason };
|
|
1343
|
+
return { ok: true };
|
|
1221
1344
|
};
|
|
1222
1345
|
|
|
1223
1346
|
// src/emailChange.ts
|
|
@@ -1247,6 +1370,14 @@ var consumeEmailChangeToken = async (token) => {
|
|
|
1247
1370
|
}
|
|
1248
1371
|
return parsed;
|
|
1249
1372
|
};
|
|
1373
|
+
var invalidateEmailChangeToken = async (userId) => {
|
|
1374
|
+
const pointerKey = emailChangeUserKey(userId);
|
|
1375
|
+
const priorKey = await redis2.get(pointerKey);
|
|
1376
|
+
if (priorKey) {
|
|
1377
|
+
await redis2.del(priorKey);
|
|
1378
|
+
}
|
|
1379
|
+
await redis2.del(pointerKey);
|
|
1380
|
+
};
|
|
1250
1381
|
|
|
1251
1382
|
// src/emailChangeNotification.ts
|
|
1252
1383
|
import { getProjectConfig as getProjectConfig8 } from "@luckystack/core";
|
|
@@ -1770,9 +1901,13 @@ var confirmTotpEnrollment = async (user, code) => {
|
|
|
1770
1901
|
}
|
|
1771
1902
|
};
|
|
1772
1903
|
var disableTwoFactor = async (user, code) => {
|
|
1904
|
+
if (await isTwoFactorAccountLocked(user.id)) return { ok: false, reason: "login.twoFactorLocked" };
|
|
1773
1905
|
const viaTotp = await verifyTotpForUser(user, code, false);
|
|
1774
1906
|
const viaRecovery = viaTotp ? false : await consumeRecoveryCode(user, code);
|
|
1775
|
-
if (!viaTotp && !viaRecovery)
|
|
1907
|
+
if (!viaTotp && !viaRecovery) {
|
|
1908
|
+
await recordTwoFactorAccountFailure(user.id);
|
|
1909
|
+
return { ok: false, reason: "login.twoFactorInvalidCode" };
|
|
1910
|
+
}
|
|
1776
1911
|
const [updateError] = await tryCatch3(() => getUserAdapter().update(user.id, {
|
|
1777
1912
|
totpSecret: null,
|
|
1778
1913
|
twoFactorEnabled: false,
|
|
@@ -1786,7 +1921,12 @@ var disableTwoFactor = async (user, code) => {
|
|
|
1786
1921
|
return { ok: true };
|
|
1787
1922
|
};
|
|
1788
1923
|
var regenerateRecoveryCodes = async (user, code) => {
|
|
1789
|
-
if (
|
|
1924
|
+
if (await isTwoFactorAccountLocked(user.id)) return { ok: false, reason: "login.twoFactorLocked" };
|
|
1925
|
+
if (!await verifyTotpForUser(user, code, false)) {
|
|
1926
|
+
await recordTwoFactorAccountFailure(user.id);
|
|
1927
|
+
return { ok: false, reason: "login.twoFactorInvalidCode" };
|
|
1928
|
+
}
|
|
1929
|
+
await clearTwoFactorAccountFailures(user.id);
|
|
1790
1930
|
const { raw, hashes } = generateRecoveryCodes();
|
|
1791
1931
|
const [updateError] = await tryCatch3(() => getUserAdapter().update(user.id, { recoveryCodes: hashes }));
|
|
1792
1932
|
if (updateError) {
|
|
@@ -1879,6 +2019,7 @@ registerSessionProvider({
|
|
|
1879
2019
|
logout: ({ token, socket, userId, skipSessionDelete }) => logout({ token, socket, userId: userId ?? null, skipSessionDelete })
|
|
1880
2020
|
});
|
|
1881
2021
|
registerAuthLockoutHook();
|
|
2022
|
+
registerRedactedLogKeys(["recoverycode", "totp", "totpcode"]);
|
|
1882
2023
|
export {
|
|
1883
2024
|
OAUTH_STATE_COOKIE_NAME,
|
|
1884
2025
|
PasswordPolicyError,
|
|
@@ -1917,6 +2058,8 @@ export {
|
|
|
1917
2058
|
githubProvider,
|
|
1918
2059
|
googleProvider,
|
|
1919
2060
|
hotp,
|
|
2061
|
+
invalidateEmailChangeToken,
|
|
2062
|
+
invalidatePasswordResetToken,
|
|
1920
2063
|
isAccountLocked,
|
|
1921
2064
|
isFullOAuthProvider,
|
|
1922
2065
|
isUserAdapterRegistered,
|