najm-auth 4.2.4 → 5.0.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/README.md +1372 -1372
- package/dist/client/index.js +17 -10
- package/dist/client/react/index.js +13 -6
- package/dist/client/server/index.js +20 -17
- package/dist/index.d.ts +120 -131
- package/dist/index.js +907 -664
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -498,7 +498,7 @@ import { Get, Post, ResMsg } from "najm-core";
|
|
|
498
498
|
import { Body, User as User2, Headers, Ctx } from "najm-core";
|
|
499
499
|
|
|
500
500
|
// src/auth/AuthService.ts
|
|
501
|
-
import { Injectable as Injectable12, Inject as
|
|
501
|
+
import { Injectable as Injectable12, Inject as Inject13 } from "najm-core";
|
|
502
502
|
import { Err as Err12, Log } from "najm-core";
|
|
503
503
|
import { Transaction as Transaction4 } from "najm-database";
|
|
504
504
|
import { I18n as I18n8, I18nService as I18nService2 } from "najm-i18n";
|
|
@@ -512,7 +512,7 @@ import { I18nService } from "najm-i18n";
|
|
|
512
512
|
import { I18n as I18n4 } from "najm-i18n";
|
|
513
513
|
|
|
514
514
|
// src/users/UserRepository.ts
|
|
515
|
-
import { eq as eq2, ne, sql as sql3 } from "drizzle-orm";
|
|
515
|
+
import { and, eq as eq2, isNull, ne, sql as sql3 } from "drizzle-orm";
|
|
516
516
|
import { Repository, Inject as Inject3 } from "najm-core";
|
|
517
517
|
import { DB } from "najm-database";
|
|
518
518
|
|
|
@@ -699,6 +699,11 @@ var UserRepository = class UserRepository2 {
|
|
|
699
699
|
const [updatedUser] = await this.db.update(this.users).set(data).where(eq2(this.users.id, id)).returning();
|
|
700
700
|
return updatedUser;
|
|
701
701
|
}
|
|
702
|
+
/** The credential that authorized a delayed write must still own the account. */
|
|
703
|
+
async updateWithCredential(id, data, expected) {
|
|
704
|
+
const [updated] = await this.db.update(this.users).set(data).where(and(eq2(this.users.id, id), eq2(this.users.password, expected.password), eq2(this.users.email, expected.email), expected.status == null ? isNull(this.users.status) : eq2(this.users.status, expected.status), expected.emailVerified == null ? isNull(this.users.emailVerified) : eq2(this.users.emailVerified, expected.emailVerified))).returning();
|
|
705
|
+
return updated;
|
|
706
|
+
}
|
|
702
707
|
async updateLastLogin(id) {
|
|
703
708
|
const [updatedUser] = await this.db.update(this.users).set({ lastLogin: (/* @__PURE__ */ new Date()).toISOString() }).where(eq2(this.users.id, id)).returning();
|
|
704
709
|
return updatedUser;
|
|
@@ -758,6 +763,24 @@ UserRepository = __decorate3([
|
|
|
758
763
|
import { Injectable as Injectable2 } from "najm-core";
|
|
759
764
|
import { Err } from "najm-core";
|
|
760
765
|
import { I18n } from "najm-i18n";
|
|
766
|
+
|
|
767
|
+
// src/shared/uniqueViolation.ts
|
|
768
|
+
function isUniqueViolation(error) {
|
|
769
|
+
const visited = /* @__PURE__ */ new Set();
|
|
770
|
+
while (error && typeof error === "object" && !visited.has(error)) {
|
|
771
|
+
visited.add(error);
|
|
772
|
+
const candidate = error;
|
|
773
|
+
if (candidate.code === "23505" || candidate.code === "SQLITE_CONSTRAINT_UNIQUE" || candidate.code === "SQLITE_CONSTRAINT_PRIMARYKEY")
|
|
774
|
+
return true;
|
|
775
|
+
if (typeof candidate.message === "string" && /unique constraint failed/i.test(candidate.message))
|
|
776
|
+
return true;
|
|
777
|
+
error = candidate.cause;
|
|
778
|
+
}
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
__name(isUniqueViolation, "isUniqueViolation");
|
|
782
|
+
|
|
783
|
+
// src/users/UserValidator.ts
|
|
761
784
|
var __decorate4 = function(decorators, target, key, desc) {
|
|
762
785
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
763
786
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -786,7 +809,7 @@ var UserValidator = class UserValidator2 {
|
|
|
786
809
|
async checkEmailUnique(email2, excludeId) {
|
|
787
810
|
if (!email2)
|
|
788
811
|
return;
|
|
789
|
-
const existingUser = await this.userRepository.
|
|
812
|
+
const existingUser = await this.userRepository.getByEmailInsensitive(email2);
|
|
790
813
|
if (existingUser && existingUser.id !== excludeId) {
|
|
791
814
|
Err(this.t("errors.emailExists"), 409);
|
|
792
815
|
}
|
|
@@ -802,6 +825,20 @@ var UserValidator = class UserValidator2 {
|
|
|
802
825
|
Err(this.t("errors.phoneExists"), 409);
|
|
803
826
|
}
|
|
804
827
|
}
|
|
828
|
+
/** The database remains authoritative when two validation reads both pass. */
|
|
829
|
+
async writeUnique(write) {
|
|
830
|
+
try {
|
|
831
|
+
return await write();
|
|
832
|
+
} catch (error) {
|
|
833
|
+
if (isUniqueViolation(error))
|
|
834
|
+
Err("User identity already exists", 409);
|
|
835
|
+
throw error;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
checkCredentialUnchanged(updated) {
|
|
839
|
+
if (!updated)
|
|
840
|
+
Err("Account credentials changed. Please sign in again or request a new link.", 409);
|
|
841
|
+
}
|
|
805
842
|
/**
|
|
806
843
|
* Check if user exists by ID
|
|
807
844
|
*/
|
|
@@ -919,7 +956,7 @@ UserValidator = __decorate4([
|
|
|
919
956
|
], UserValidator);
|
|
920
957
|
|
|
921
958
|
// src/roles/RoleService.ts
|
|
922
|
-
import { Injectable as
|
|
959
|
+
import { Injectable as Injectable5, Err as Err3 } from "najm-core";
|
|
923
960
|
import { I18n as I18n3 } from "najm-i18n";
|
|
924
961
|
|
|
925
962
|
// src/roles/RoleRepository.ts
|
|
@@ -1069,7 +1106,16 @@ RoleValidator = __decorate6([
|
|
|
1069
1106
|
__metadata6("design:paramtypes", [typeof (_a3 = typeof RoleRepository !== "undefined" && RoleRepository) === "function" ? _a3 : Object])
|
|
1070
1107
|
], RoleValidator);
|
|
1071
1108
|
|
|
1072
|
-
// src/
|
|
1109
|
+
// src/tokens/SessionInvalidationService.ts
|
|
1110
|
+
import { Inject as Inject6, Injectable as Injectable4 } from "najm-core";
|
|
1111
|
+
import { CacheService } from "najm-cache";
|
|
1112
|
+
import { TransactionService } from "najm-database";
|
|
1113
|
+
import timestring2 from "timestring";
|
|
1114
|
+
|
|
1115
|
+
// src/tokens/TokenRepository.ts
|
|
1116
|
+
import { and as and2, eq as eq4, isNull as isNull2, lt } from "drizzle-orm";
|
|
1117
|
+
import { Repository as Repository3, Inject as Inject5 } from "najm-core";
|
|
1118
|
+
import { DB as DB3 } from "najm-database";
|
|
1073
1119
|
var __decorate7 = function(decorators, target, key, desc) {
|
|
1074
1120
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1075
1121
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1079,487 +1125,177 @@ var __decorate7 = function(decorators, target, key, desc) {
|
|
|
1079
1125
|
var __metadata7 = function(k, v) {
|
|
1080
1126
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
1081
1127
|
};
|
|
1082
|
-
var
|
|
1083
|
-
var _b2;
|
|
1084
|
-
var RoleService = class RoleService2 {
|
|
1128
|
+
var TokenRepository = class TokenRepository2 {
|
|
1085
1129
|
static {
|
|
1086
|
-
__name(this, "
|
|
1130
|
+
__name(this, "TokenRepository");
|
|
1087
1131
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
this.roleRepository = roleRepository;
|
|
1093
|
-
this.roleValidator = roleValidator;
|
|
1132
|
+
db;
|
|
1133
|
+
schema;
|
|
1134
|
+
get tokens() {
|
|
1135
|
+
return this.schema.tokens;
|
|
1094
1136
|
}
|
|
1095
|
-
|
|
1096
|
-
return
|
|
1137
|
+
get users() {
|
|
1138
|
+
return this.schema.users;
|
|
1097
1139
|
}
|
|
1098
|
-
async
|
|
1099
|
-
await this.
|
|
1100
|
-
|
|
1140
|
+
async getCredentialState(userId) {
|
|
1141
|
+
const [user] = await this.db.select({
|
|
1142
|
+
id: this.users.id,
|
|
1143
|
+
password: this.users.password,
|
|
1144
|
+
email: this.users.email,
|
|
1145
|
+
status: this.users.status,
|
|
1146
|
+
emailVerified: this.users.emailVerified
|
|
1147
|
+
}).from(this.users).where(eq4(this.users.id, userId)).limit(1);
|
|
1148
|
+
return user;
|
|
1101
1149
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1150
|
+
/** Shared query helper, scoped to the current database/transaction identity. */
|
|
1151
|
+
queryHelper;
|
|
1152
|
+
get q() {
|
|
1153
|
+
const db = this.db;
|
|
1154
|
+
if (this.queryHelper?.db !== db) {
|
|
1155
|
+
this.queryHelper = { db, queries: new AuthQueries(db, this.schema) };
|
|
1156
|
+
}
|
|
1157
|
+
return this.queryHelper.queries;
|
|
1104
1158
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1159
|
+
/**
|
|
1160
|
+
* Upsert the refresh-token row for a session, keyed on `tokenFamily` (the
|
|
1161
|
+
* per-login session identifier, unique). A brand-new login inserts a fresh
|
|
1162
|
+
* family row; a refresh rotation updates only that family's row, leaving the
|
|
1163
|
+
* user's other sessions untouched.
|
|
1164
|
+
*/
|
|
1165
|
+
async storeRefreshToken(tokenData) {
|
|
1166
|
+
return await this.db.insert(this.tokens).values(tokenData).onConflictDoUpdate({
|
|
1167
|
+
target: this.tokens.tokenFamily,
|
|
1168
|
+
set: {
|
|
1169
|
+
token: tokenData.token,
|
|
1170
|
+
expiresAt: tokenData.expiresAt,
|
|
1171
|
+
previousHash: tokenData.previousHash ?? null,
|
|
1172
|
+
previousValidUntil: tokenData.previousValidUntil ?? null,
|
|
1173
|
+
previousUsedAt: tokenData.previousUsedAt ?? null
|
|
1174
|
+
},
|
|
1175
|
+
// A revoked family is a durable tombstone. It must never be revived by
|
|
1176
|
+
// an issuance racing with logout, or after the cache markers are lost.
|
|
1177
|
+
setWhere: and2(eq4(this.tokens.status, "active"), eq4(this.tokens.userId, tokenData.userId))
|
|
1178
|
+
}).returning();
|
|
1108
1179
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
await this.
|
|
1115
|
-
|
|
1180
|
+
/**
|
|
1181
|
+
* Rotate an existing refresh-token family with compare-and-swap semantics.
|
|
1182
|
+
* This can never update a family durably revoked by a concurrent logout.
|
|
1183
|
+
*/
|
|
1184
|
+
async rotateRefreshToken(tokenData, expectedCurrentHash) {
|
|
1185
|
+
return await this.db.update(this.tokens).set({
|
|
1186
|
+
token: tokenData.token,
|
|
1187
|
+
expiresAt: tokenData.expiresAt,
|
|
1188
|
+
previousHash: tokenData.previousHash,
|
|
1189
|
+
previousValidUntil: tokenData.previousValidUntil,
|
|
1190
|
+
previousUsedAt: tokenData.previousUsedAt ?? null
|
|
1191
|
+
}).where(and2(eq4(this.tokens.tokenFamily, tokenData.tokenFamily), eq4(this.tokens.userId, tokenData.userId), eq4(this.tokens.token, expectedCurrentHash), eq4(this.tokens.status, "active"))).returning();
|
|
1116
1192
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1193
|
+
/**
|
|
1194
|
+
* Claim the previous-token grace slot for a single family. Conditional on
|
|
1195
|
+
* BOTH the stored previousHash still matching the presented token AND
|
|
1196
|
+
* previousUsedAt being NULL. Gating on the hash (not just the flag) closes
|
|
1197
|
+
* the rotation race: the winner's rotation rewrites previousHash via
|
|
1198
|
+
* storeRefreshToken (and resets previousUsedAt to NULL), so a loser whose
|
|
1199
|
+
* UPDATE lands after that rotation no longer matches and gets zero rows —
|
|
1200
|
+
* exactly one caller ever claims the slot.
|
|
1201
|
+
*/
|
|
1202
|
+
async markPreviousUsed(tokenFamily, previousHash) {
|
|
1203
|
+
return await this.db.update(this.tokens).set({ previousUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and2(eq4(this.tokens.tokenFamily, tokenFamily), eq4(this.tokens.previousHash, previousHash), isNull2(this.tokens.previousUsedAt), eq4(this.tokens.status, "active"))).returning();
|
|
1126
1204
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
if (!exists) {
|
|
1132
|
-
rolesToCreate.push(role);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
const createdRoles = await Promise.all(rolesToCreate.map((role) => this.roleRepository.create(role)));
|
|
1136
|
-
return createdRoles;
|
|
1205
|
+
/** Look up a single session's token row by its family identifier. */
|
|
1206
|
+
async getByFamily(tokenFamily) {
|
|
1207
|
+
const [token] = await this.db.select().from(this.tokens).where(and2(eq4(this.tokens.tokenFamily, tokenFamily), eq4(this.tokens.status, "active")));
|
|
1208
|
+
return token ?? null;
|
|
1137
1209
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1210
|
+
/**
|
|
1211
|
+
* Durably revoke one family without depending on physical deletion.
|
|
1212
|
+
*
|
|
1213
|
+
* The row remains as a tombstone until its original refresh expiry. This is
|
|
1214
|
+
* what prevents a later cache loss from turning a failed/omitted cleanup
|
|
1215
|
+
* delete into a valid database-backed recovery session.
|
|
1216
|
+
*/
|
|
1217
|
+
async revokeFamily(tokenFamily) {
|
|
1218
|
+
return this.db.update(this.tokens).set({
|
|
1219
|
+
status: "revoked",
|
|
1220
|
+
previousHash: null,
|
|
1221
|
+
previousValidUntil: null,
|
|
1222
|
+
previousUsedAt: null
|
|
1223
|
+
}).where(and2(eq4(this.tokens.tokenFamily, tokenFamily), eq4(this.tokens.status, "active"))).returning();
|
|
1224
|
+
}
|
|
1225
|
+
/** Durably revoke every active family for a user. */
|
|
1226
|
+
async revokeAllForUser(userId) {
|
|
1227
|
+
return this.db.update(this.tokens).set({
|
|
1228
|
+
status: "revoked",
|
|
1229
|
+
previousHash: null,
|
|
1230
|
+
previousValidUntil: null,
|
|
1231
|
+
previousUsedAt: null
|
|
1232
|
+
}).where(and2(eq4(this.tokens.userId, userId), eq4(this.tokens.status, "active"))).returning();
|
|
1141
1233
|
}
|
|
1142
1234
|
/**
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1145
|
-
* loser; answer it with the same 409 the validator would have given rather
|
|
1146
|
-
* than leaking a raw driver error as a 500.
|
|
1235
|
+
* Opportunistic cleanup: with one row per family (no unique userId), expired
|
|
1236
|
+
* and abandoned sessions accumulate. Delete every expired row.
|
|
1147
1237
|
*/
|
|
1148
|
-
async
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1238
|
+
async deleteExpired() {
|
|
1239
|
+
return this.db.delete(this.tokens).where(lt(this.tokens.expiresAt, (/* @__PURE__ */ new Date()).toISOString())).returning();
|
|
1240
|
+
}
|
|
1241
|
+
async isUserExists(userId) {
|
|
1242
|
+
const [user] = await this.db.select({ id: this.users.id }).from(this.users).where(eq4(this.users.id, userId)).limit(1);
|
|
1243
|
+
return !!user;
|
|
1244
|
+
}
|
|
1245
|
+
async getRoleNameById(userId) {
|
|
1246
|
+
return this.q.getRoleName(userId);
|
|
1247
|
+
}
|
|
1248
|
+
async getUserPermissions(userId) {
|
|
1249
|
+
return this.q.getUserPermissions(userId);
|
|
1250
|
+
}
|
|
1251
|
+
async getRoleAndPermissions(userId) {
|
|
1252
|
+
return this.q.getRoleAndPermissions(userId);
|
|
1253
|
+
}
|
|
1254
|
+
async getUser(userId) {
|
|
1255
|
+
return await this.q.getUserWithPermissions(eq4(this.users.id, userId)) ?? null;
|
|
1157
1256
|
}
|
|
1158
1257
|
};
|
|
1159
1258
|
__decorate7([
|
|
1160
|
-
|
|
1259
|
+
DB3(),
|
|
1161
1260
|
__metadata7("design:type", Object)
|
|
1162
|
-
],
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
__metadata7("design:
|
|
1166
|
-
],
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
const candidate = error;
|
|
1171
|
-
if (candidate.code === "23505")
|
|
1172
|
-
return true;
|
|
1173
|
-
if (candidate.code === "SQLITE_CONSTRAINT_UNIQUE")
|
|
1174
|
-
return true;
|
|
1175
|
-
const message = typeof candidate.message === "string" ? candidate.message : "";
|
|
1176
|
-
return /unique constraint failed/i.test(message) || /duplicate key value violates unique constraint/i.test(message);
|
|
1177
|
-
}
|
|
1178
|
-
__name(isDuplicateRoleName, "isDuplicateRoleName");
|
|
1179
|
-
|
|
1180
|
-
// src/users/UserService.ts
|
|
1181
|
-
import { nanoid as nanoid3 } from "nanoid";
|
|
1261
|
+
], TokenRepository.prototype, "db", void 0);
|
|
1262
|
+
__decorate7([
|
|
1263
|
+
Inject5(AUTH_SCHEMA),
|
|
1264
|
+
__metadata7("design:type", Object)
|
|
1265
|
+
], TokenRepository.prototype, "schema", void 0);
|
|
1266
|
+
TokenRepository = __decorate7([
|
|
1267
|
+
Repository3()
|
|
1268
|
+
], TokenRepository);
|
|
1182
1269
|
|
|
1183
|
-
// src/
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1270
|
+
// src/tokens/SessionInvalidationService.ts
|
|
1271
|
+
var __decorate8 = function(decorators, target, key, desc) {
|
|
1272
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1273
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
1274
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
1275
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1276
|
+
};
|
|
1277
|
+
var __metadata8 = function(k, v) {
|
|
1278
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
1279
|
+
};
|
|
1280
|
+
var SessionInvalidationService_1;
|
|
1281
|
+
var _a4;
|
|
1282
|
+
var _b2;
|
|
1283
|
+
var _c;
|
|
1284
|
+
var SessionInvalidationService = class SessionInvalidationService2 {
|
|
1285
|
+
static {
|
|
1286
|
+
__name(this, "SessionInvalidationService");
|
|
1196
1287
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
const cleaned = {};
|
|
1200
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
1201
|
-
if (value !== void 0 && value !== "") {
|
|
1202
|
-
cleaned[key] = value;
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
return cleaned;
|
|
1206
|
-
}, "clean");
|
|
1207
|
-
var getAvatarFile = /* @__PURE__ */ __name(async (fileName) => {
|
|
1208
|
-
try {
|
|
1209
|
-
const filePath = path.join(avatarsPath, fileName);
|
|
1210
|
-
const buffer = await fs.readFile(filePath);
|
|
1211
|
-
const file = new File([buffer], fileName, {
|
|
1212
|
-
type: "image/png"
|
|
1213
|
-
});
|
|
1214
|
-
return file;
|
|
1215
|
-
} catch (error) {
|
|
1216
|
-
return null;
|
|
1217
|
-
}
|
|
1218
|
-
}, "getAvatarFile");
|
|
1219
|
-
var formatDate = /* @__PURE__ */ __name((dateValue) => {
|
|
1220
|
-
if (!dateValue)
|
|
1221
|
-
return null;
|
|
1222
|
-
let date;
|
|
1223
|
-
if (dateValue instanceof Date) {
|
|
1224
|
-
date = dateValue;
|
|
1225
|
-
} else if (typeof dateValue === "string") {
|
|
1226
|
-
date = new Date(dateValue);
|
|
1227
|
-
} else {
|
|
1228
|
-
return null;
|
|
1229
|
-
}
|
|
1230
|
-
if (isNaN(date.getTime()))
|
|
1231
|
-
return null;
|
|
1232
|
-
return date.toISOString().split("T")[0];
|
|
1233
|
-
}, "formatDate");
|
|
1234
|
-
function calculateAge(dateOfBirth) {
|
|
1235
|
-
if (!dateOfBirth)
|
|
1236
|
-
return null;
|
|
1237
|
-
const formattedDate = formatDate(dateOfBirth);
|
|
1238
|
-
if (!formattedDate)
|
|
1239
|
-
return null;
|
|
1240
|
-
const birth = new Date(formattedDate);
|
|
1241
|
-
const today = /* @__PURE__ */ new Date();
|
|
1242
|
-
let age = today.getFullYear() - birth.getFullYear();
|
|
1243
|
-
const monthDiff = today.getMonth() - birth.getMonth();
|
|
1244
|
-
if (monthDiff < 0 || monthDiff === 0 && today.getDate() < birth.getDate()) {
|
|
1245
|
-
age--;
|
|
1246
|
-
}
|
|
1247
|
-
return age;
|
|
1248
|
-
}
|
|
1249
|
-
__name(calculateAge, "calculateAge");
|
|
1250
|
-
function calculateYearsOfExperience(hireDate) {
|
|
1251
|
-
if (!hireDate)
|
|
1252
|
-
return null;
|
|
1253
|
-
const formattedDate = formatDate(hireDate);
|
|
1254
|
-
if (!formattedDate)
|
|
1255
|
-
return null;
|
|
1256
|
-
const hire = new Date(formattedDate);
|
|
1257
|
-
const today = /* @__PURE__ */ new Date();
|
|
1258
|
-
let years = today.getFullYear() - hire.getFullYear();
|
|
1259
|
-
const monthDiff = today.getMonth() - hire.getMonth();
|
|
1260
|
-
if (monthDiff < 0 || monthDiff === 0 && today.getDate() < hire.getDate()) {
|
|
1261
|
-
years--;
|
|
1262
|
-
}
|
|
1263
|
-
return years;
|
|
1264
|
-
}
|
|
1265
|
-
__name(calculateYearsOfExperience, "calculateYearsOfExperience");
|
|
1266
|
-
function pickProps(source, keys) {
|
|
1267
|
-
const result = {};
|
|
1268
|
-
for (const key of keys) {
|
|
1269
|
-
if (source[key] !== void 0) {
|
|
1270
|
-
result[key] = source[key];
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
return result;
|
|
1274
|
-
}
|
|
1275
|
-
__name(pickProps, "pickProps");
|
|
1276
|
-
var isEmpty = _isEmpty;
|
|
1277
|
-
var isPath = /* @__PURE__ */ __name((img) => typeof img === "string" && img.trim().length > 0 && (img.startsWith("/") || img.startsWith("http") || img.startsWith("storage/")), "isPath");
|
|
1278
|
-
var isFile = /* @__PURE__ */ __name((img) => !!img && typeof img !== "string" && img instanceof File, "isFile");
|
|
1279
|
-
|
|
1280
|
-
// src/identity/presets.ts
|
|
1281
|
-
var compactPhone = /* @__PURE__ */ __name((value) => value.trim().replace(/[\s().-]+/g, ""), "compactPhone");
|
|
1282
|
-
var MOROCCO_LOCAL = /^0\d{9}$/;
|
|
1283
|
-
var MOROCCO_NATIONAL = /^212\d{9}$/;
|
|
1284
|
-
var MOROCCO_E164 = /^\+212\d{9}$/;
|
|
1285
|
-
var normalizeMoroccanPhone = /* @__PURE__ */ __name((value) => {
|
|
1286
|
-
const compact = compactPhone(value);
|
|
1287
|
-
if (MOROCCO_LOCAL.test(compact))
|
|
1288
|
-
return `+212${compact.slice(1)}`;
|
|
1289
|
-
if (MOROCCO_NATIONAL.test(compact))
|
|
1290
|
-
return `+${compact}`;
|
|
1291
|
-
if (MOROCCO_E164.test(compact))
|
|
1292
|
-
return compact;
|
|
1293
|
-
return null;
|
|
1294
|
-
}, "normalizeMoroccanPhone");
|
|
1295
|
-
var moroccoIdentityPreset = {
|
|
1296
|
-
name: "ma",
|
|
1297
|
-
normalize: normalizeMoroccanPhone
|
|
1298
|
-
};
|
|
1299
|
-
var TUNISIA_LOCAL = /^\d{8}$/;
|
|
1300
|
-
var TUNISIA_NATIONAL = /^216\d{8}$/;
|
|
1301
|
-
var TUNISIA_E164 = /^\+216\d{8}$/;
|
|
1302
|
-
var normalizeTunisianPhone = /* @__PURE__ */ __name((value) => {
|
|
1303
|
-
const compact = compactPhone(value);
|
|
1304
|
-
if (TUNISIA_LOCAL.test(compact))
|
|
1305
|
-
return `+216${compact}`;
|
|
1306
|
-
if (TUNISIA_NATIONAL.test(compact))
|
|
1307
|
-
return `+${compact}`;
|
|
1308
|
-
if (TUNISIA_E164.test(compact))
|
|
1309
|
-
return compact;
|
|
1310
|
-
return null;
|
|
1311
|
-
}, "normalizeTunisianPhone");
|
|
1312
|
-
var tunisiaIdentityPreset = {
|
|
1313
|
-
name: "tn",
|
|
1314
|
-
normalize: normalizeTunisianPhone
|
|
1315
|
-
};
|
|
1316
|
-
var IDENTITY_PRESETS = {
|
|
1317
|
-
ma: moroccoIdentityPreset,
|
|
1318
|
-
tn: tunisiaIdentityPreset
|
|
1319
|
-
};
|
|
1320
|
-
var DEFAULT_IDENTITY_PRESET = "ma";
|
|
1321
|
-
|
|
1322
|
-
// src/identity/resolver.ts
|
|
1323
|
-
var MAX_IDENTIFIER_LENGTH = 254;
|
|
1324
|
-
var E164 = /^\+[1-9]\d{7,14}$/;
|
|
1325
|
-
var normalizeE164 = /* @__PURE__ */ __name((value) => {
|
|
1326
|
-
const compact = compactPhone(value);
|
|
1327
|
-
const international = compact.startsWith("00") ? `+${compact.slice(2)}` : compact;
|
|
1328
|
-
return E164.test(international) ? international : null;
|
|
1329
|
-
}, "normalizeE164");
|
|
1330
|
-
var selectPreset = /* @__PURE__ */ __name((preset) => {
|
|
1331
|
-
if (preset === null)
|
|
1332
|
-
return null;
|
|
1333
|
-
if (preset === void 0)
|
|
1334
|
-
return IDENTITY_PRESETS[DEFAULT_IDENTITY_PRESET];
|
|
1335
|
-
if (typeof preset === "string") {
|
|
1336
|
-
const selected = IDENTITY_PRESETS[preset];
|
|
1337
|
-
if (!selected)
|
|
1338
|
-
throw new Error(`auth.identity.preset '${preset}' is not a built-in identity preset`);
|
|
1339
|
-
return selected;
|
|
1340
|
-
}
|
|
1341
|
-
if (typeof preset.normalize !== "function") {
|
|
1342
|
-
throw new Error("auth.identity.preset must expose a normalize(value) function");
|
|
1343
|
-
}
|
|
1344
|
-
return preset;
|
|
1345
|
-
}, "selectPreset");
|
|
1346
|
-
function createIdentityResolver(config) {
|
|
1347
|
-
const preset = selectPreset(config?.preset);
|
|
1348
|
-
const extend = config?.extend ?? [];
|
|
1349
|
-
if (extend.some((normalizer) => typeof normalizer !== "function")) {
|
|
1350
|
-
throw new Error("auth.identity.extend must contain only normalizer functions");
|
|
1351
|
-
}
|
|
1352
|
-
const pipeline = [
|
|
1353
|
-
...extend,
|
|
1354
|
-
...preset ? [preset.normalize] : [],
|
|
1355
|
-
normalizeE164
|
|
1356
|
-
];
|
|
1357
|
-
return (value) => {
|
|
1358
|
-
if (typeof value !== "string")
|
|
1359
|
-
return null;
|
|
1360
|
-
const trimmed = value.trim();
|
|
1361
|
-
if (!trimmed || trimmed.length > MAX_IDENTIFIER_LENGTH)
|
|
1362
|
-
return null;
|
|
1363
|
-
if (trimmed.includes("@"))
|
|
1364
|
-
return trimmed.toLowerCase();
|
|
1365
|
-
for (const normalize of pipeline) {
|
|
1366
|
-
const normalized = normalize(trimmed);
|
|
1367
|
-
if (normalized)
|
|
1368
|
-
return normalized;
|
|
1369
|
-
}
|
|
1370
|
-
return null;
|
|
1371
|
-
};
|
|
1372
|
-
}
|
|
1373
|
-
__name(createIdentityResolver, "createIdentityResolver");
|
|
1374
|
-
var defaultResolver = createIdentityResolver();
|
|
1375
|
-
function normalizeAuthIdentifier(value) {
|
|
1376
|
-
return defaultResolver(value);
|
|
1377
|
-
}
|
|
1378
|
-
__name(normalizeAuthIdentifier, "normalizeAuthIdentifier");
|
|
1379
|
-
function isEmailIdentifier(value) {
|
|
1380
|
-
return value.includes("@");
|
|
1381
|
-
}
|
|
1382
|
-
__name(isEmailIdentifier, "isEmailIdentifier");
|
|
1383
|
-
|
|
1384
|
-
// src/users/UserService.ts
|
|
1385
|
-
import { Err as Err5 } from "najm-core";
|
|
1386
|
-
|
|
1387
|
-
// src/tokens/SessionInvalidationService.ts
|
|
1388
|
-
import { Inject as Inject6, Injectable as Injectable5 } from "najm-core";
|
|
1389
|
-
import { CacheService } from "najm-cache";
|
|
1390
|
-
import timestring2 from "timestring";
|
|
1391
|
-
|
|
1392
|
-
// src/tokens/TokenRepository.ts
|
|
1393
|
-
import { and, eq as eq4, isNull, lt } from "drizzle-orm";
|
|
1394
|
-
import { Repository as Repository3, Inject as Inject5 } from "najm-core";
|
|
1395
|
-
import { DB as DB3 } from "najm-database";
|
|
1396
|
-
var __decorate8 = function(decorators, target, key, desc) {
|
|
1397
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1398
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
1399
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
1400
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1401
|
-
};
|
|
1402
|
-
var __metadata8 = function(k, v) {
|
|
1403
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
1404
|
-
};
|
|
1405
|
-
var TokenRepository = class TokenRepository2 {
|
|
1406
|
-
static {
|
|
1407
|
-
__name(this, "TokenRepository");
|
|
1408
|
-
}
|
|
1409
|
-
db;
|
|
1410
|
-
schema;
|
|
1411
|
-
get tokens() {
|
|
1412
|
-
return this.schema.tokens;
|
|
1413
|
-
}
|
|
1414
|
-
get users() {
|
|
1415
|
-
return this.schema.users;
|
|
1416
|
-
}
|
|
1417
|
-
/** Shared query helper, scoped to the current database/transaction identity. */
|
|
1418
|
-
queryHelper;
|
|
1419
|
-
get q() {
|
|
1420
|
-
const db = this.db;
|
|
1421
|
-
if (this.queryHelper?.db !== db) {
|
|
1422
|
-
this.queryHelper = { db, queries: new AuthQueries(db, this.schema) };
|
|
1423
|
-
}
|
|
1424
|
-
return this.queryHelper.queries;
|
|
1425
|
-
}
|
|
1426
|
-
/**
|
|
1427
|
-
* Upsert the refresh-token row for a session, keyed on `tokenFamily` (the
|
|
1428
|
-
* per-login session identifier, unique). A brand-new login inserts a fresh
|
|
1429
|
-
* family row; a refresh rotation updates only that family's row, leaving the
|
|
1430
|
-
* user's other sessions untouched.
|
|
1431
|
-
*/
|
|
1432
|
-
async storeRefreshToken(tokenData) {
|
|
1433
|
-
return await this.db.insert(this.tokens).values(tokenData).onConflictDoUpdate({
|
|
1434
|
-
target: this.tokens.tokenFamily,
|
|
1435
|
-
set: {
|
|
1436
|
-
token: tokenData.token,
|
|
1437
|
-
expiresAt: tokenData.expiresAt,
|
|
1438
|
-
previousHash: tokenData.previousHash ?? null,
|
|
1439
|
-
previousValidUntil: tokenData.previousValidUntil ?? null,
|
|
1440
|
-
previousUsedAt: tokenData.previousUsedAt ?? null
|
|
1441
|
-
},
|
|
1442
|
-
// A revoked family is a durable tombstone. It must never be revived by
|
|
1443
|
-
// an issuance racing with logout, or after the cache markers are lost.
|
|
1444
|
-
setWhere: and(eq4(this.tokens.status, "active"), eq4(this.tokens.userId, tokenData.userId))
|
|
1445
|
-
}).returning();
|
|
1446
|
-
}
|
|
1447
|
-
/**
|
|
1448
|
-
* Rotate an existing refresh-token family with compare-and-swap semantics.
|
|
1449
|
-
* This can never update a family durably revoked by a concurrent logout.
|
|
1450
|
-
*/
|
|
1451
|
-
async rotateRefreshToken(tokenData, expectedCurrentHash) {
|
|
1452
|
-
return await this.db.update(this.tokens).set({
|
|
1453
|
-
token: tokenData.token,
|
|
1454
|
-
expiresAt: tokenData.expiresAt,
|
|
1455
|
-
previousHash: tokenData.previousHash,
|
|
1456
|
-
previousValidUntil: tokenData.previousValidUntil,
|
|
1457
|
-
previousUsedAt: tokenData.previousUsedAt ?? null
|
|
1458
|
-
}).where(and(eq4(this.tokens.tokenFamily, tokenData.tokenFamily), eq4(this.tokens.userId, tokenData.userId), eq4(this.tokens.token, expectedCurrentHash), eq4(this.tokens.status, "active"))).returning();
|
|
1459
|
-
}
|
|
1460
|
-
/**
|
|
1461
|
-
* Claim the previous-token grace slot for a single family. Conditional on
|
|
1462
|
-
* BOTH the stored previousHash still matching the presented token AND
|
|
1463
|
-
* previousUsedAt being NULL. Gating on the hash (not just the flag) closes
|
|
1464
|
-
* the rotation race: the winner's rotation rewrites previousHash via
|
|
1465
|
-
* storeRefreshToken (and resets previousUsedAt to NULL), so a loser whose
|
|
1466
|
-
* UPDATE lands after that rotation no longer matches and gets zero rows —
|
|
1467
|
-
* exactly one caller ever claims the slot.
|
|
1468
|
-
*/
|
|
1469
|
-
async markPreviousUsed(tokenFamily, previousHash) {
|
|
1470
|
-
return await this.db.update(this.tokens).set({ previousUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and(eq4(this.tokens.tokenFamily, tokenFamily), eq4(this.tokens.previousHash, previousHash), isNull(this.tokens.previousUsedAt), eq4(this.tokens.status, "active"))).returning();
|
|
1471
|
-
}
|
|
1472
|
-
/** Look up a single session's token row by its family identifier. */
|
|
1473
|
-
async getByFamily(tokenFamily) {
|
|
1474
|
-
const [token] = await this.db.select().from(this.tokens).where(and(eq4(this.tokens.tokenFamily, tokenFamily), eq4(this.tokens.status, "active")));
|
|
1475
|
-
return token ?? null;
|
|
1476
|
-
}
|
|
1477
|
-
/**
|
|
1478
|
-
* Durably revoke one family without depending on physical deletion.
|
|
1479
|
-
*
|
|
1480
|
-
* The row remains as a tombstone until its original refresh expiry. This is
|
|
1481
|
-
* what prevents a later cache loss from turning a failed/omitted cleanup
|
|
1482
|
-
* delete into a valid database-backed recovery session.
|
|
1483
|
-
*/
|
|
1484
|
-
async revokeFamily(tokenFamily) {
|
|
1485
|
-
return this.db.update(this.tokens).set({
|
|
1486
|
-
status: "revoked",
|
|
1487
|
-
previousHash: null,
|
|
1488
|
-
previousValidUntil: null,
|
|
1489
|
-
previousUsedAt: null
|
|
1490
|
-
}).where(and(eq4(this.tokens.tokenFamily, tokenFamily), eq4(this.tokens.status, "active"))).returning();
|
|
1491
|
-
}
|
|
1492
|
-
/** Durably revoke every active family for a user. */
|
|
1493
|
-
async revokeAllForUser(userId) {
|
|
1494
|
-
return this.db.update(this.tokens).set({
|
|
1495
|
-
status: "revoked",
|
|
1496
|
-
previousHash: null,
|
|
1497
|
-
previousValidUntil: null,
|
|
1498
|
-
previousUsedAt: null
|
|
1499
|
-
}).where(and(eq4(this.tokens.userId, userId), eq4(this.tokens.status, "active"))).returning();
|
|
1500
|
-
}
|
|
1501
|
-
/**
|
|
1502
|
-
* Opportunistic cleanup: with one row per family (no unique userId), expired
|
|
1503
|
-
* and abandoned sessions accumulate. Delete every expired row.
|
|
1504
|
-
*/
|
|
1505
|
-
async deleteExpired() {
|
|
1506
|
-
return this.db.delete(this.tokens).where(lt(this.tokens.expiresAt, (/* @__PURE__ */ new Date()).toISOString())).returning();
|
|
1507
|
-
}
|
|
1508
|
-
async isUserExists(userId) {
|
|
1509
|
-
const [user] = await this.db.select({ id: this.users.id }).from(this.users).where(eq4(this.users.id, userId)).limit(1);
|
|
1510
|
-
return !!user;
|
|
1511
|
-
}
|
|
1512
|
-
async getRoleNameById(userId) {
|
|
1513
|
-
return this.q.getRoleName(userId);
|
|
1514
|
-
}
|
|
1515
|
-
async getUserPermissions(userId) {
|
|
1516
|
-
return this.q.getUserPermissions(userId);
|
|
1517
|
-
}
|
|
1518
|
-
async getRoleAndPermissions(userId) {
|
|
1519
|
-
return this.q.getRoleAndPermissions(userId);
|
|
1520
|
-
}
|
|
1521
|
-
async getUser(userId) {
|
|
1522
|
-
return await this.q.getUserWithPermissions(eq4(this.users.id, userId)) ?? null;
|
|
1523
|
-
}
|
|
1524
|
-
};
|
|
1525
|
-
__decorate8([
|
|
1526
|
-
DB3(),
|
|
1527
|
-
__metadata8("design:type", Object)
|
|
1528
|
-
], TokenRepository.prototype, "db", void 0);
|
|
1529
|
-
__decorate8([
|
|
1530
|
-
Inject5(AUTH_SCHEMA),
|
|
1531
|
-
__metadata8("design:type", Object)
|
|
1532
|
-
], TokenRepository.prototype, "schema", void 0);
|
|
1533
|
-
TokenRepository = __decorate8([
|
|
1534
|
-
Repository3()
|
|
1535
|
-
], TokenRepository);
|
|
1536
|
-
|
|
1537
|
-
// src/tokens/SessionInvalidationService.ts
|
|
1538
|
-
var __decorate9 = function(decorators, target, key, desc) {
|
|
1539
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1540
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
1541
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
1542
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1543
|
-
};
|
|
1544
|
-
var __metadata9 = function(k, v) {
|
|
1545
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
1546
|
-
};
|
|
1547
|
-
var SessionInvalidationService_1;
|
|
1548
|
-
var _a5;
|
|
1549
|
-
var _b3;
|
|
1550
|
-
var SessionInvalidationService = class SessionInvalidationService2 {
|
|
1551
|
-
static {
|
|
1552
|
-
__name(this, "SessionInvalidationService");
|
|
1553
|
-
}
|
|
1554
|
-
static {
|
|
1555
|
-
SessionInvalidationService_1 = this;
|
|
1288
|
+
static {
|
|
1289
|
+
SessionInvalidationService_1 = this;
|
|
1556
1290
|
}
|
|
1557
1291
|
cache;
|
|
1558
1292
|
tokens;
|
|
1293
|
+
transactions;
|
|
1559
1294
|
config;
|
|
1560
|
-
constructor(cache2, tokens) {
|
|
1295
|
+
constructor(cache2, tokens, transactions) {
|
|
1561
1296
|
this.cache = cache2;
|
|
1562
1297
|
this.tokens = tokens;
|
|
1298
|
+
this.transactions = transactions;
|
|
1563
1299
|
}
|
|
1564
1300
|
/**
|
|
1565
1301
|
* Fields whose change ends existing sessions. Everything absent from this set
|
|
@@ -1582,7 +1318,7 @@ var SessionInvalidationService = class SessionInvalidationService2 {
|
|
|
1582
1318
|
return SessionInvalidationService_1.SECURITY_FIELDS.some((field) => data[field] !== void 0);
|
|
1583
1319
|
}
|
|
1584
1320
|
get accessTokenTtlMs() {
|
|
1585
|
-
return timestring2(this.config.jwt.accessExpiresIn, "ms");
|
|
1321
|
+
return Math.max(timestring2(this.config.jwt.accessExpiresIn, "ms"), (this.config.session?.maxAge ?? 300) * 1e3);
|
|
1586
1322
|
}
|
|
1587
1323
|
get refreshTokenTtlMs() {
|
|
1588
1324
|
return timestring2(this.config.jwt.refreshExpiresIn, "ms");
|
|
@@ -1636,9 +1372,15 @@ var SessionInvalidationService = class SessionInvalidationService2 {
|
|
|
1636
1372
|
* the same version and write the same successor back.
|
|
1637
1373
|
*/
|
|
1638
1374
|
async invalidateAccessTokens(userId) {
|
|
1375
|
+
const version = await this.invalidateAccessTokensNow(userId);
|
|
1376
|
+
await this.afterTransaction(() => this.invalidateAccessTokens(userId));
|
|
1377
|
+
return version;
|
|
1378
|
+
}
|
|
1379
|
+
async invalidateAccessTokensNow(userId) {
|
|
1639
1380
|
const key = this.sessionVersionKey(userId);
|
|
1640
1381
|
const { count } = await this.cache.incr(key, this.accessTokenTtlMs);
|
|
1641
1382
|
await this.cache.expire(key, this.accessTokenTtlMs);
|
|
1383
|
+
await this.cache.del(`${this.userCacheKey(userId)}:${count - 1}`);
|
|
1642
1384
|
await this.dropUserCache(userId);
|
|
1643
1385
|
return count;
|
|
1644
1386
|
}
|
|
@@ -1649,15 +1391,30 @@ var SessionInvalidationService = class SessionInvalidationService2 {
|
|
|
1649
1391
|
* End every session a user holds: access tokens by version, refresh sessions
|
|
1650
1392
|
* by row, and each family's liveness marker.
|
|
1651
1393
|
*
|
|
1652
|
-
*
|
|
1653
|
-
*
|
|
1654
|
-
*
|
|
1655
|
-
* after a rollback merely signs the user out again, which is safe.
|
|
1394
|
+
* Repeat invalidation when a caller-owned transaction completes. Readers can
|
|
1395
|
+
* recover from the previous committed state before commit, or from transient
|
|
1396
|
+
* state on a shared connection before rollback. Neither snapshot may survive.
|
|
1656
1397
|
*/
|
|
1657
1398
|
async invalidateUser(userId) {
|
|
1658
|
-
await this.
|
|
1399
|
+
await this.invalidateAccessTokensNow(userId);
|
|
1400
|
+
await this.revokeAllForUserNow(userId);
|
|
1401
|
+
await this.afterTransaction(() => this.invalidateUser(userId));
|
|
1402
|
+
}
|
|
1403
|
+
async revokeAllForUser(userId) {
|
|
1404
|
+
const revoked = await this.revokeAllForUserNow(userId);
|
|
1405
|
+
await this.afterTransaction(() => this.revokeAllForUser(userId));
|
|
1406
|
+
return revoked;
|
|
1407
|
+
}
|
|
1408
|
+
async afterTransaction(callback) {
|
|
1409
|
+
if (!this.transactions?.isActive())
|
|
1410
|
+
return;
|
|
1411
|
+
await this.transactions.afterCommit(callback);
|
|
1412
|
+
await this.transactions.afterRollback(callback);
|
|
1413
|
+
}
|
|
1414
|
+
async revokeAllForUserNow(userId) {
|
|
1659
1415
|
const revoked = await this.tokens.revokeAllForUser(userId);
|
|
1660
1416
|
await Promise.all(familiesOf(revoked).map((family) => this.markFamilyRevoked(family)));
|
|
1417
|
+
return revoked;
|
|
1661
1418
|
}
|
|
1662
1419
|
/**
|
|
1663
1420
|
* Record that a family is live, and whose it is. Called wherever the family's
|
|
@@ -1722,40 +1479,365 @@ var SessionInvalidationService = class SessionInvalidationService2 {
|
|
|
1722
1479
|
async isFamilyLive(tokenFamily, userId) {
|
|
1723
1480
|
return await this.familyStatus(tokenFamily, userId) === "live";
|
|
1724
1481
|
}
|
|
1725
|
-
async readMany(keys) {
|
|
1726
|
-
const cache2 = this.cache;
|
|
1727
|
-
if (cache2.getMany)
|
|
1728
|
-
return cache2.getMany(keys);
|
|
1729
|
-
return Promise.all(keys.map((key) => cache2.get(key)));
|
|
1482
|
+
async readMany(keys) {
|
|
1483
|
+
const cache2 = this.cache;
|
|
1484
|
+
if (cache2.getMany)
|
|
1485
|
+
return cache2.getMany(keys);
|
|
1486
|
+
return Promise.all(keys.map((key) => cache2.get(key)));
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Keep revocation through both credential lifetimes. If deleting the refresh
|
|
1490
|
+
* row fails, its still-valid cookie must remain denied after access expires.
|
|
1491
|
+
*/
|
|
1492
|
+
async markFamilyRevoked(tokenFamily) {
|
|
1493
|
+
await this.cache.set(this.revokedFamilyKey(tokenFamily), "1", Math.max(this.accessTokenTtlMs, this.refreshTokenTtlMs));
|
|
1494
|
+
await this.cache.del(this.familyKey(tokenFamily));
|
|
1495
|
+
}
|
|
1496
|
+
async isFamilyRevoked(tokenFamily) {
|
|
1497
|
+
return await this.cache.get(this.revokedFamilyKey(tokenFamily)) !== null;
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
__decorate8([
|
|
1501
|
+
Inject6(AUTH_CONFIG),
|
|
1502
|
+
__metadata8("design:type", Object)
|
|
1503
|
+
], SessionInvalidationService.prototype, "config", void 0);
|
|
1504
|
+
SessionInvalidationService = SessionInvalidationService_1 = __decorate8([
|
|
1505
|
+
Injectable4(),
|
|
1506
|
+
__metadata8("design:paramtypes", [typeof (_a4 = typeof CacheService !== "undefined" && CacheService) === "function" ? _a4 : Object, typeof (_b2 = typeof TokenRepository !== "undefined" && TokenRepository) === "function" ? _b2 : Object, typeof (_c = typeof TransactionService !== "undefined" && TransactionService) === "function" ? _c : Object])
|
|
1507
|
+
], SessionInvalidationService);
|
|
1508
|
+
function familiesOf(rows) {
|
|
1509
|
+
if (!Array.isArray(rows))
|
|
1510
|
+
return [];
|
|
1511
|
+
return rows.map((row) => row?.tokenFamily).filter((family) => typeof family === "string" && family.length > 0);
|
|
1512
|
+
}
|
|
1513
|
+
__name(familiesOf, "familiesOf");
|
|
1514
|
+
|
|
1515
|
+
// src/roles/RoleService.ts
|
|
1516
|
+
var __decorate9 = function(decorators, target, key, desc) {
|
|
1517
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1518
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
1519
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
1520
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1521
|
+
};
|
|
1522
|
+
var __metadata9 = function(k, v) {
|
|
1523
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
1524
|
+
};
|
|
1525
|
+
var _a5;
|
|
1526
|
+
var _b3;
|
|
1527
|
+
var _c2;
|
|
1528
|
+
var _d;
|
|
1529
|
+
var RoleService = class RoleService2 {
|
|
1530
|
+
static {
|
|
1531
|
+
__name(this, "RoleService");
|
|
1532
|
+
}
|
|
1533
|
+
roleRepository;
|
|
1534
|
+
roleValidator;
|
|
1535
|
+
userRepository;
|
|
1536
|
+
sessionInvalidation;
|
|
1537
|
+
t;
|
|
1538
|
+
constructor(roleRepository, roleValidator, userRepository, sessionInvalidation) {
|
|
1539
|
+
this.roleRepository = roleRepository;
|
|
1540
|
+
this.roleValidator = roleValidator;
|
|
1541
|
+
this.userRepository = userRepository;
|
|
1542
|
+
this.sessionInvalidation = sessionInvalidation;
|
|
1543
|
+
}
|
|
1544
|
+
async getAll() {
|
|
1545
|
+
return await this.roleRepository.getAll();
|
|
1546
|
+
}
|
|
1547
|
+
async getById(id) {
|
|
1548
|
+
await this.roleValidator.checkRoleExists(id);
|
|
1549
|
+
return await this.roleRepository.getById(id);
|
|
1550
|
+
}
|
|
1551
|
+
async getByName(name) {
|
|
1552
|
+
return await this.roleRepository.getByName(name);
|
|
1553
|
+
}
|
|
1554
|
+
async create(data) {
|
|
1555
|
+
await this.roleValidator.checkNameUnique(data.name);
|
|
1556
|
+
return await this.onDuplicateName(() => this.roleRepository.create(data));
|
|
1557
|
+
}
|
|
1558
|
+
async update(id, data) {
|
|
1559
|
+
const role = await this.roleValidator.checkRoleExists(id);
|
|
1560
|
+
if (role.name === ROLES.ADMIN && data.name && data.name !== ROLES.ADMIN) {
|
|
1561
|
+
Err3(this.t("errors.cannotRenameSystem"), 403);
|
|
1562
|
+
}
|
|
1563
|
+
await this.roleValidator.checkNameUnique(data.name, id);
|
|
1564
|
+
const updated = await this.onDuplicateName(() => this.roleRepository.update(id, data));
|
|
1565
|
+
if (data.name !== void 0 && this.userRepository && this.sessionInvalidation) {
|
|
1566
|
+
for (const userId of await this.userRepository.getIdsByRole(id)) {
|
|
1567
|
+
await this.sessionInvalidation.invalidateAccessTokens(userId);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
return updated;
|
|
1571
|
+
}
|
|
1572
|
+
async delete(id) {
|
|
1573
|
+
const role = await this.roleValidator.checkRoleExists(id);
|
|
1574
|
+
if (role.name === ROLES.ADMIN) {
|
|
1575
|
+
Err3(this.t("errors.cannotDeleteSystem"), 403);
|
|
1576
|
+
}
|
|
1577
|
+
if (await this.roleRepository.hasUsers(id)) {
|
|
1578
|
+
Err3(this.t("errors.roleInUse"), 409);
|
|
1579
|
+
}
|
|
1580
|
+
return await this.roleRepository.delete(id);
|
|
1581
|
+
}
|
|
1582
|
+
async seedDefaultRoles(defaultRoles) {
|
|
1583
|
+
const rolesToCreate = [];
|
|
1584
|
+
for (const role of defaultRoles) {
|
|
1585
|
+
const exists = await this.roleValidator.isRoleNameExists(role.name);
|
|
1586
|
+
if (!exists) {
|
|
1587
|
+
rolesToCreate.push(role);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
const createdRoles = await Promise.all(rolesToCreate.map((role) => this.roleRepository.create(role)));
|
|
1591
|
+
return createdRoles;
|
|
1592
|
+
}
|
|
1593
|
+
async getRoleIdByName(name) {
|
|
1594
|
+
const role = await this.getByName(name);
|
|
1595
|
+
return role?.id;
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* checkNameUnique() reads before it writes, so two concurrent requests can
|
|
1599
|
+
* both pass it and race to insert the same name. The database rejects the
|
|
1600
|
+
* loser; answer it with the same 409 the validator would have given rather
|
|
1601
|
+
* than leaking a raw driver error as a 500.
|
|
1602
|
+
*/
|
|
1603
|
+
async onDuplicateName(write) {
|
|
1604
|
+
try {
|
|
1605
|
+
return await write();
|
|
1606
|
+
} catch (error) {
|
|
1607
|
+
if (isDuplicateRoleName(error)) {
|
|
1608
|
+
Err3(this.t("errors.exists"), 409);
|
|
1609
|
+
}
|
|
1610
|
+
throw error;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
};
|
|
1614
|
+
__decorate9([
|
|
1615
|
+
I18n3("roles"),
|
|
1616
|
+
__metadata9("design:type", Object)
|
|
1617
|
+
], RoleService.prototype, "t", void 0);
|
|
1618
|
+
RoleService = __decorate9([
|
|
1619
|
+
Injectable5(),
|
|
1620
|
+
__metadata9("design:paramtypes", [typeof (_a5 = typeof RoleRepository !== "undefined" && RoleRepository) === "function" ? _a5 : Object, typeof (_b3 = typeof RoleValidator !== "undefined" && RoleValidator) === "function" ? _b3 : Object, typeof (_c2 = typeof UserRepository !== "undefined" && UserRepository) === "function" ? _c2 : Object, typeof (_d = typeof SessionInvalidationService !== "undefined" && SessionInvalidationService) === "function" ? _d : Object])
|
|
1621
|
+
], RoleService);
|
|
1622
|
+
function isDuplicateRoleName(error) {
|
|
1623
|
+
if (!error || typeof error !== "object")
|
|
1624
|
+
return false;
|
|
1625
|
+
const candidate = error;
|
|
1626
|
+
if (candidate.code === "23505")
|
|
1627
|
+
return true;
|
|
1628
|
+
if (candidate.code === "SQLITE_CONSTRAINT_UNIQUE")
|
|
1629
|
+
return true;
|
|
1630
|
+
const message = typeof candidate.message === "string" ? candidate.message : "";
|
|
1631
|
+
return /unique constraint failed/i.test(message) || /duplicate key value violates unique constraint/i.test(message);
|
|
1632
|
+
}
|
|
1633
|
+
__name(isDuplicateRoleName, "isDuplicateRoleName");
|
|
1634
|
+
|
|
1635
|
+
// src/users/UserService.ts
|
|
1636
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
1637
|
+
|
|
1638
|
+
// src/shared/index.ts
|
|
1639
|
+
import * as fs from "fs/promises";
|
|
1640
|
+
import * as path from "path";
|
|
1641
|
+
import _isEmpty from "lodash.isempty";
|
|
1642
|
+
import { Err as Err4 } from "najm-core";
|
|
1643
|
+
var avatarsPath = path.join(process.cwd(), "avatars");
|
|
1644
|
+
var parseSchema = /* @__PURE__ */ __name(async (schema, data) => {
|
|
1645
|
+
try {
|
|
1646
|
+
return await schema.parseAsync(data);
|
|
1647
|
+
} catch (error) {
|
|
1648
|
+
const errors = error.issues || error.errors || [];
|
|
1649
|
+
const errorMessage = errors.map((err) => `${err.path.join(".")}: ${err.message}`).join("; ");
|
|
1650
|
+
Err4(errorMessage);
|
|
1651
|
+
}
|
|
1652
|
+
}, "parseSchema");
|
|
1653
|
+
var clean = /* @__PURE__ */ __name((obj) => {
|
|
1654
|
+
const cleaned = {};
|
|
1655
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1656
|
+
if (value !== void 0 && value !== "") {
|
|
1657
|
+
cleaned[key] = value;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
return cleaned;
|
|
1661
|
+
}, "clean");
|
|
1662
|
+
var getAvatarFile = /* @__PURE__ */ __name(async (fileName) => {
|
|
1663
|
+
try {
|
|
1664
|
+
const filePath = path.join(avatarsPath, fileName);
|
|
1665
|
+
const buffer = await fs.readFile(filePath);
|
|
1666
|
+
const file = new File([buffer], fileName, {
|
|
1667
|
+
type: "image/png"
|
|
1668
|
+
});
|
|
1669
|
+
return file;
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
return null;
|
|
1672
|
+
}
|
|
1673
|
+
}, "getAvatarFile");
|
|
1674
|
+
var formatDate = /* @__PURE__ */ __name((dateValue) => {
|
|
1675
|
+
if (!dateValue)
|
|
1676
|
+
return null;
|
|
1677
|
+
let date;
|
|
1678
|
+
if (dateValue instanceof Date) {
|
|
1679
|
+
date = dateValue;
|
|
1680
|
+
} else if (typeof dateValue === "string") {
|
|
1681
|
+
date = new Date(dateValue);
|
|
1682
|
+
} else {
|
|
1683
|
+
return null;
|
|
1684
|
+
}
|
|
1685
|
+
if (isNaN(date.getTime()))
|
|
1686
|
+
return null;
|
|
1687
|
+
return date.toISOString().split("T")[0];
|
|
1688
|
+
}, "formatDate");
|
|
1689
|
+
function calculateAge(dateOfBirth) {
|
|
1690
|
+
if (!dateOfBirth)
|
|
1691
|
+
return null;
|
|
1692
|
+
const formattedDate = formatDate(dateOfBirth);
|
|
1693
|
+
if (!formattedDate)
|
|
1694
|
+
return null;
|
|
1695
|
+
const birth = new Date(formattedDate);
|
|
1696
|
+
const today = /* @__PURE__ */ new Date();
|
|
1697
|
+
let age = today.getFullYear() - birth.getFullYear();
|
|
1698
|
+
const monthDiff = today.getMonth() - birth.getMonth();
|
|
1699
|
+
if (monthDiff < 0 || monthDiff === 0 && today.getDate() < birth.getDate()) {
|
|
1700
|
+
age--;
|
|
1701
|
+
}
|
|
1702
|
+
return age;
|
|
1703
|
+
}
|
|
1704
|
+
__name(calculateAge, "calculateAge");
|
|
1705
|
+
function calculateYearsOfExperience(hireDate) {
|
|
1706
|
+
if (!hireDate)
|
|
1707
|
+
return null;
|
|
1708
|
+
const formattedDate = formatDate(hireDate);
|
|
1709
|
+
if (!formattedDate)
|
|
1710
|
+
return null;
|
|
1711
|
+
const hire = new Date(formattedDate);
|
|
1712
|
+
const today = /* @__PURE__ */ new Date();
|
|
1713
|
+
let years = today.getFullYear() - hire.getFullYear();
|
|
1714
|
+
const monthDiff = today.getMonth() - hire.getMonth();
|
|
1715
|
+
if (monthDiff < 0 || monthDiff === 0 && today.getDate() < hire.getDate()) {
|
|
1716
|
+
years--;
|
|
1717
|
+
}
|
|
1718
|
+
return years;
|
|
1719
|
+
}
|
|
1720
|
+
__name(calculateYearsOfExperience, "calculateYearsOfExperience");
|
|
1721
|
+
function pickProps(source, keys) {
|
|
1722
|
+
const result = {};
|
|
1723
|
+
for (const key of keys) {
|
|
1724
|
+
if (source[key] !== void 0) {
|
|
1725
|
+
result[key] = source[key];
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
return result;
|
|
1729
|
+
}
|
|
1730
|
+
__name(pickProps, "pickProps");
|
|
1731
|
+
var isEmpty = _isEmpty;
|
|
1732
|
+
var isPath = /* @__PURE__ */ __name((img) => typeof img === "string" && img.trim().length > 0 && (img.startsWith("/") || img.startsWith("http") || img.startsWith("storage/")), "isPath");
|
|
1733
|
+
var isFile = /* @__PURE__ */ __name((img) => !!img && typeof img !== "string" && img instanceof File, "isFile");
|
|
1734
|
+
|
|
1735
|
+
// src/identity/presets.ts
|
|
1736
|
+
var compactPhone = /* @__PURE__ */ __name((value) => value.trim().replace(/[\s().-]+/g, ""), "compactPhone");
|
|
1737
|
+
var MOROCCO_LOCAL = /^0\d{9}$/;
|
|
1738
|
+
var MOROCCO_NATIONAL = /^212\d{9}$/;
|
|
1739
|
+
var MOROCCO_E164 = /^\+212\d{9}$/;
|
|
1740
|
+
var normalizeMoroccanPhone = /* @__PURE__ */ __name((value) => {
|
|
1741
|
+
const compact = compactPhone(value);
|
|
1742
|
+
if (MOROCCO_LOCAL.test(compact))
|
|
1743
|
+
return `+212${compact.slice(1)}`;
|
|
1744
|
+
if (MOROCCO_NATIONAL.test(compact))
|
|
1745
|
+
return `+${compact}`;
|
|
1746
|
+
if (MOROCCO_E164.test(compact))
|
|
1747
|
+
return compact;
|
|
1748
|
+
return null;
|
|
1749
|
+
}, "normalizeMoroccanPhone");
|
|
1750
|
+
var moroccoIdentityPreset = {
|
|
1751
|
+
name: "ma",
|
|
1752
|
+
normalize: normalizeMoroccanPhone
|
|
1753
|
+
};
|
|
1754
|
+
var TUNISIA_LOCAL = /^\d{8}$/;
|
|
1755
|
+
var TUNISIA_NATIONAL = /^216\d{8}$/;
|
|
1756
|
+
var TUNISIA_E164 = /^\+216\d{8}$/;
|
|
1757
|
+
var normalizeTunisianPhone = /* @__PURE__ */ __name((value) => {
|
|
1758
|
+
const compact = compactPhone(value);
|
|
1759
|
+
if (TUNISIA_LOCAL.test(compact))
|
|
1760
|
+
return `+216${compact}`;
|
|
1761
|
+
if (TUNISIA_NATIONAL.test(compact))
|
|
1762
|
+
return `+${compact}`;
|
|
1763
|
+
if (TUNISIA_E164.test(compact))
|
|
1764
|
+
return compact;
|
|
1765
|
+
return null;
|
|
1766
|
+
}, "normalizeTunisianPhone");
|
|
1767
|
+
var tunisiaIdentityPreset = {
|
|
1768
|
+
name: "tn",
|
|
1769
|
+
normalize: normalizeTunisianPhone
|
|
1770
|
+
};
|
|
1771
|
+
var IDENTITY_PRESETS = {
|
|
1772
|
+
ma: moroccoIdentityPreset,
|
|
1773
|
+
tn: tunisiaIdentityPreset
|
|
1774
|
+
};
|
|
1775
|
+
var DEFAULT_IDENTITY_PRESET = "ma";
|
|
1776
|
+
|
|
1777
|
+
// src/identity/resolver.ts
|
|
1778
|
+
var MAX_IDENTIFIER_LENGTH = 254;
|
|
1779
|
+
var E164 = /^\+[1-9]\d{7,14}$/;
|
|
1780
|
+
var normalizeE164 = /* @__PURE__ */ __name((value) => {
|
|
1781
|
+
const compact = compactPhone(value);
|
|
1782
|
+
const international = compact.startsWith("00") ? `+${compact.slice(2)}` : compact;
|
|
1783
|
+
return E164.test(international) ? international : null;
|
|
1784
|
+
}, "normalizeE164");
|
|
1785
|
+
var selectPreset = /* @__PURE__ */ __name((preset) => {
|
|
1786
|
+
if (preset === null)
|
|
1787
|
+
return null;
|
|
1788
|
+
if (preset === void 0)
|
|
1789
|
+
return IDENTITY_PRESETS[DEFAULT_IDENTITY_PRESET];
|
|
1790
|
+
if (typeof preset === "string") {
|
|
1791
|
+
const selected = IDENTITY_PRESETS[preset];
|
|
1792
|
+
if (!selected)
|
|
1793
|
+
throw new Error(`auth.identity.preset '${preset}' is not a built-in identity preset`);
|
|
1794
|
+
return selected;
|
|
1730
1795
|
}
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
* row fails, its still-valid cookie must remain denied after access expires.
|
|
1734
|
-
*/
|
|
1735
|
-
async markFamilyRevoked(tokenFamily) {
|
|
1736
|
-
await this.cache.set(this.revokedFamilyKey(tokenFamily), "1", Math.max(this.accessTokenTtlMs, this.refreshTokenTtlMs));
|
|
1737
|
-
await this.cache.del(this.familyKey(tokenFamily));
|
|
1796
|
+
if (typeof preset.normalize !== "function") {
|
|
1797
|
+
throw new Error("auth.identity.preset must expose a normalize(value) function");
|
|
1738
1798
|
}
|
|
1739
|
-
|
|
1740
|
-
|
|
1799
|
+
return preset;
|
|
1800
|
+
}, "selectPreset");
|
|
1801
|
+
function createIdentityResolver(config) {
|
|
1802
|
+
const preset = selectPreset(config?.preset);
|
|
1803
|
+
const extend = config?.extend ?? [];
|
|
1804
|
+
if (extend.some((normalizer) => typeof normalizer !== "function")) {
|
|
1805
|
+
throw new Error("auth.identity.extend must contain only normalizer functions");
|
|
1741
1806
|
}
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
]
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1807
|
+
const pipeline = [
|
|
1808
|
+
...extend,
|
|
1809
|
+
...preset ? [preset.normalize] : [],
|
|
1810
|
+
normalizeE164
|
|
1811
|
+
];
|
|
1812
|
+
return (value) => {
|
|
1813
|
+
if (typeof value !== "string")
|
|
1814
|
+
return null;
|
|
1815
|
+
const trimmed = value.trim();
|
|
1816
|
+
if (!trimmed || trimmed.length > MAX_IDENTIFIER_LENGTH)
|
|
1817
|
+
return null;
|
|
1818
|
+
if (trimmed.includes("@"))
|
|
1819
|
+
return trimmed.toLowerCase();
|
|
1820
|
+
for (const normalize of pipeline) {
|
|
1821
|
+
const normalized = normalize(trimmed);
|
|
1822
|
+
if (normalized)
|
|
1823
|
+
return normalized;
|
|
1824
|
+
}
|
|
1825
|
+
return null;
|
|
1826
|
+
};
|
|
1755
1827
|
}
|
|
1756
|
-
__name(
|
|
1828
|
+
__name(createIdentityResolver, "createIdentityResolver");
|
|
1829
|
+
var defaultResolver = createIdentityResolver();
|
|
1830
|
+
function normalizeAuthIdentifier(value) {
|
|
1831
|
+
return defaultResolver(value);
|
|
1832
|
+
}
|
|
1833
|
+
__name(normalizeAuthIdentifier, "normalizeAuthIdentifier");
|
|
1834
|
+
function isEmailIdentifier(value) {
|
|
1835
|
+
return value.includes("@");
|
|
1836
|
+
}
|
|
1837
|
+
__name(isEmailIdentifier, "isEmailIdentifier");
|
|
1757
1838
|
|
|
1758
1839
|
// src/users/UserService.ts
|
|
1840
|
+
import { Err as Err5 } from "najm-core";
|
|
1759
1841
|
var __decorate10 = function(decorators, target, key, desc) {
|
|
1760
1842
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1761
1843
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1772,8 +1854,8 @@ var __param2 = function(paramIndex, decorator) {
|
|
|
1772
1854
|
};
|
|
1773
1855
|
var _a6;
|
|
1774
1856
|
var _b4;
|
|
1775
|
-
var
|
|
1776
|
-
var
|
|
1857
|
+
var _c3;
|
|
1858
|
+
var _d2;
|
|
1777
1859
|
var _e;
|
|
1778
1860
|
var _f;
|
|
1779
1861
|
var _g;
|
|
@@ -1905,7 +1987,7 @@ var UserService = class UserService2 {
|
|
|
1905
1987
|
const userDetails = {
|
|
1906
1988
|
id: userId,
|
|
1907
1989
|
name: name || null,
|
|
1908
|
-
email: email2,
|
|
1990
|
+
email: email2.trim().toLowerCase(),
|
|
1909
1991
|
image,
|
|
1910
1992
|
password: hashedPassword,
|
|
1911
1993
|
roleId: resolvedRoleId,
|
|
@@ -1913,7 +1995,7 @@ var UserService = class UserService2 {
|
|
|
1913
1995
|
...normalizedPhone ? { phone: normalizedPhone } : {},
|
|
1914
1996
|
status: data.status ?? this.authConfig.registrationMode ?? "active"
|
|
1915
1997
|
};
|
|
1916
|
-
const newUser = await this.userRepository.create(userDetails);
|
|
1998
|
+
const newUser = await this.userValidator.writeUnique(() => this.userRepository.create(userDetails));
|
|
1917
1999
|
return this.sanitizeUser(newUser);
|
|
1918
2000
|
}
|
|
1919
2001
|
/**
|
|
@@ -1939,8 +2021,19 @@ var UserService = class UserService2 {
|
|
|
1939
2021
|
image,
|
|
1940
2022
|
...hashedPassword && { password: hashedPassword }
|
|
1941
2023
|
};
|
|
2024
|
+
if (typeof data.email === "string")
|
|
2025
|
+
updateData.email = data.email.trim().toLowerCase();
|
|
2026
|
+
if (data.phone !== void 0 && data.phone !== null) {
|
|
2027
|
+
const phone = this.authConfig.identity?.resolve(data.phone) ?? normalizeAuthIdentifier(data.phone);
|
|
2028
|
+
if (!phone || !E164_PHONE.test(phone))
|
|
2029
|
+
Err5("A valid phone number is required", 400);
|
|
2030
|
+
await this.userValidator.checkPhoneUnique(phone, id);
|
|
2031
|
+
updateData.phone = phone;
|
|
2032
|
+
}
|
|
1942
2033
|
const cleanedUpdateData = clean(updateData);
|
|
1943
|
-
const updatedUser = await this.userRepository.update(id, cleanedUpdateData);
|
|
2034
|
+
const updatedUser = await this.userValidator.writeUnique(() => options.expectedCredential ? this.userRepository.updateWithCredential(id, cleanedUpdateData, options.expectedCredential) : this.userRepository.update(id, cleanedUpdateData));
|
|
2035
|
+
if (options.expectedCredential)
|
|
2036
|
+
this.userValidator.checkCredentialUnchanged(updatedUser);
|
|
1944
2037
|
const result = this.sanitizeUser(this.requireUser(updatedUser));
|
|
1945
2038
|
if (SessionInvalidationService.affectsSecurityState(cleanedUpdateData)) {
|
|
1946
2039
|
await this.invalidateSessions(id);
|
|
@@ -2033,20 +2126,20 @@ __decorate10([
|
|
|
2033
2126
|
UserService = __decorate10([
|
|
2034
2127
|
Injectable6(),
|
|
2035
2128
|
__param2(6, Inject7(AUTH_CONFIG)),
|
|
2036
|
-
__metadata10("design:paramtypes", [typeof (_a6 = typeof RoleValidator !== "undefined" && RoleValidator) === "function" ? _a6 : Object, typeof (_b4 = typeof RoleService !== "undefined" && RoleService) === "function" ? _b4 : Object, typeof (
|
|
2129
|
+
__metadata10("design:paramtypes", [typeof (_a6 = typeof RoleValidator !== "undefined" && RoleValidator) === "function" ? _a6 : Object, typeof (_b4 = typeof RoleService !== "undefined" && RoleService) === "function" ? _b4 : Object, typeof (_c3 = typeof UserRepository !== "undefined" && UserRepository) === "function" ? _c3 : Object, typeof (_d2 = typeof UserValidator !== "undefined" && UserValidator) === "function" ? _d2 : Object, typeof (_e = typeof EncryptionService !== "undefined" && EncryptionService) === "function" ? _e : Object, typeof (_f = typeof I18nService !== "undefined" && I18nService) === "function" ? _f : Object, Object, typeof (_g = typeof SessionInvalidationService !== "undefined" && SessionInvalidationService) === "function" ? _g : Object])
|
|
2037
2130
|
], UserService);
|
|
2038
2131
|
|
|
2039
2132
|
// src/tokens/TokenService.ts
|
|
2040
2133
|
import { Injectable as Injectable7, Inject as Inject9 } from "najm-core";
|
|
2041
2134
|
import { I18n as I18n5 } from "najm-i18n";
|
|
2042
2135
|
import { CacheService as CacheService2 } from "najm-cache";
|
|
2043
|
-
import { createHash, createHmac } from "crypto";
|
|
2136
|
+
import { createHash, createHmac as createHmac2 } from "crypto";
|
|
2044
2137
|
import jwt from "jsonwebtoken";
|
|
2045
2138
|
import { nanoid as nanoid4 } from "nanoid";
|
|
2046
2139
|
import timestring3 from "timestring";
|
|
2047
2140
|
|
|
2048
2141
|
// src/credentialSetup/CredentialSetupRequirementRepository.ts
|
|
2049
|
-
import { and as
|
|
2142
|
+
import { and as and3, eq as eq5 } from "drizzle-orm";
|
|
2050
2143
|
import { Err as Err6, Inject as Inject8, Repository as Repository4 } from "najm-core";
|
|
2051
2144
|
import { DB as DB4 } from "najm-database";
|
|
2052
2145
|
var __decorate11 = function(decorators, target, key, desc) {
|
|
@@ -2090,16 +2183,16 @@ var CredentialSetupRequirementRepository = class CredentialSetupRequirementRepos
|
|
|
2090
2183
|
return requirement;
|
|
2091
2184
|
}
|
|
2092
2185
|
async find(userId, purpose) {
|
|
2093
|
-
const [requirement] = await this.db.select(this.columns).from(this.requirements).where(
|
|
2186
|
+
const [requirement] = await this.db.select(this.columns).from(this.requirements).where(and3(eq5(this.requirements.userId, userId), eq5(this.requirements.purpose, purpose))).limit(1);
|
|
2094
2187
|
return requirement;
|
|
2095
2188
|
}
|
|
2096
2189
|
async listRequired(userId) {
|
|
2097
|
-
return this.db.select(this.columns).from(this.requirements).where(
|
|
2190
|
+
return this.db.select(this.columns).from(this.requirements).where(and3(eq5(this.requirements.userId, userId), eq5(this.requirements.required, true)));
|
|
2098
2191
|
}
|
|
2099
2192
|
/** Only a still-required row completes, so a replayed completion is a no-op. */
|
|
2100
2193
|
async complete(userId, purpose) {
|
|
2101
2194
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2102
|
-
const [requirement] = await this.db.update(this.requirements).set({ required: false, completedAt: now, updatedAt: now }).where(
|
|
2195
|
+
const [requirement] = await this.db.update(this.requirements).set({ required: false, completedAt: now, updatedAt: now }).where(and3(eq5(this.requirements.userId, userId), eq5(this.requirements.purpose, purpose), eq5(this.requirements.required, true))).returning(this.columns);
|
|
2103
2196
|
return requirement;
|
|
2104
2197
|
}
|
|
2105
2198
|
};
|
|
@@ -2138,6 +2231,15 @@ __name(storedTimeMs, "storedTimeMs");
|
|
|
2138
2231
|
|
|
2139
2232
|
// src/tokens/TokenService.ts
|
|
2140
2233
|
import { Err as Err7 } from "najm-core";
|
|
2234
|
+
|
|
2235
|
+
// src/auth/credentialState.ts
|
|
2236
|
+
import { createHmac } from "crypto";
|
|
2237
|
+
function credentialFingerprint(secret, user) {
|
|
2238
|
+
return createHmac("sha256", secret).update("najm:set-password:v1:").update(JSON.stringify([user.id, user.password, user.email, user.status, user.emailVerified])).digest("base64url");
|
|
2239
|
+
}
|
|
2240
|
+
__name(credentialFingerprint, "credentialFingerprint");
|
|
2241
|
+
|
|
2242
|
+
// src/tokens/TokenService.ts
|
|
2141
2243
|
var __decorate12 = function(decorators, target, key, desc) {
|
|
2142
2244
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2143
2245
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -2150,8 +2252,8 @@ var __metadata12 = function(k, v) {
|
|
|
2150
2252
|
var TokenService_1;
|
|
2151
2253
|
var _a7;
|
|
2152
2254
|
var _b5;
|
|
2153
|
-
var
|
|
2154
|
-
var
|
|
2255
|
+
var _c4;
|
|
2256
|
+
var _d3;
|
|
2155
2257
|
var _e2;
|
|
2156
2258
|
var TokenService = class TokenService2 {
|
|
2157
2259
|
static {
|
|
@@ -2220,10 +2322,13 @@ var TokenService = class TokenService2 {
|
|
|
2220
2322
|
async verifyAccessToken(token) {
|
|
2221
2323
|
let payload;
|
|
2222
2324
|
try {
|
|
2223
|
-
payload = jwt.verify(token, this.config.jwt.accessSecret);
|
|
2325
|
+
payload = jwt.verify(token, this.config.jwt.accessSecret, { algorithms: ["HS256"] });
|
|
2224
2326
|
} catch {
|
|
2225
2327
|
Err7(this.t("errors.tokenVerificationFailed"), 401);
|
|
2226
2328
|
}
|
|
2329
|
+
if (!payload || typeof payload !== "object" || "type" in payload && payload.type !== "access" || typeof payload.userId !== "string" || !payload.userId || typeof payload.tokenFamily !== "string" || !payload.tokenFamily || !Number.isFinite(payload.exp) || payload.sessionVersion !== void 0 && (!Number.isSafeInteger(payload.sessionVersion) || payload.sessionVersion < 0)) {
|
|
2330
|
+
Err7(this.t("errors.tokenVerificationFailed"), 401);
|
|
2331
|
+
}
|
|
2227
2332
|
const sessionKey = this.sessionVersionKey(payload.userId);
|
|
2228
2333
|
const blacklistKey = payload.jti ? `${this.blacklistPrefix}${payload.jti}` : null;
|
|
2229
2334
|
const revokedKey = payload.tokenFamily ? this.revokedFamilyKey(payload.tokenFamily) : null;
|
|
@@ -2255,14 +2360,14 @@ var TokenService = class TokenService2 {
|
|
|
2255
2360
|
verifyRefreshToken(token) {
|
|
2256
2361
|
let decoded;
|
|
2257
2362
|
try {
|
|
2258
|
-
decoded = jwt.verify(token, this.config.jwt.refreshSecret);
|
|
2363
|
+
decoded = jwt.verify(token, this.config.jwt.refreshSecret, { algorithms: ["HS256"] });
|
|
2259
2364
|
} catch {
|
|
2260
2365
|
Err7(this.t("errors.tokenVerificationFailed"), 401);
|
|
2261
2366
|
}
|
|
2262
|
-
if (decoded
|
|
2367
|
+
if (!decoded || typeof decoded !== "object" || decoded.type !== "refresh" || typeof decoded.userId !== "string" || !decoded.userId || !Number.isFinite(decoded.exp)) {
|
|
2263
2368
|
Err7(this.t("errors.tokenVerificationFailed"), 401);
|
|
2264
2369
|
}
|
|
2265
|
-
if (!decoded.tokenFamily) {
|
|
2370
|
+
if (typeof decoded.tokenFamily !== "string" || !decoded.tokenFamily) {
|
|
2266
2371
|
Err7(this.t("errors.tokenVerificationFailed"), 401);
|
|
2267
2372
|
}
|
|
2268
2373
|
return { userId: decoded.userId, tokenFamily: decoded.tokenFamily };
|
|
@@ -2329,8 +2434,9 @@ var TokenService = class TokenService2 {
|
|
|
2329
2434
|
*/
|
|
2330
2435
|
async recoverSessionFromCookie() {
|
|
2331
2436
|
const { userId, tokenFamily } = await this.resolveRefreshSessionFromCookie();
|
|
2332
|
-
const user = await this.requireActiveRefreshUser(userId, tokenFamily);
|
|
2333
2437
|
const sessionVersion = await this.getUserSessionVersion(userId);
|
|
2438
|
+
const user = await this.requireActiveRefreshUser(userId, tokenFamily);
|
|
2439
|
+
await this.assertSessionVersion(userId, sessionVersion);
|
|
2334
2440
|
return {
|
|
2335
2441
|
user,
|
|
2336
2442
|
roles: user.role ? [user.role] : [],
|
|
@@ -2367,16 +2473,18 @@ var TokenService = class TokenService2 {
|
|
|
2367
2473
|
return user;
|
|
2368
2474
|
}
|
|
2369
2475
|
async getUserById(userId) {
|
|
2370
|
-
const
|
|
2476
|
+
const version = await this.getUserSessionVersion(userId);
|
|
2477
|
+
const cacheKey = `auth:user:${userId}:${version}`;
|
|
2371
2478
|
if (typeof this.cache.getOrSet === "function") {
|
|
2372
|
-
|
|
2479
|
+
const user2 = await this.cache.getOrSet(cacheKey, () => this.tokenRepository.getUser(userId), 3e4);
|
|
2480
|
+
await this.assertSessionVersion(userId, version);
|
|
2481
|
+
return user2;
|
|
2373
2482
|
}
|
|
2374
2483
|
const cached = await this.cache.get(cacheKey);
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
const user = await this.tokenRepository.getUser(userId);
|
|
2378
|
-
if (user)
|
|
2484
|
+
const user = cached ? JSON.parse(cached) : await this.tokenRepository.getUser(userId);
|
|
2485
|
+
if (!cached && user)
|
|
2379
2486
|
await this.cache.set(cacheKey, JSON.stringify(user), 3e4);
|
|
2487
|
+
await this.assertSessionVersion(userId, version);
|
|
2380
2488
|
return user;
|
|
2381
2489
|
}
|
|
2382
2490
|
// ============ TOKEN GENERATION ============
|
|
@@ -2389,9 +2497,9 @@ var TokenService = class TokenService2 {
|
|
|
2389
2497
|
decodeAccessToken(token) {
|
|
2390
2498
|
return jwt.decode(token);
|
|
2391
2499
|
}
|
|
2392
|
-
async signAccessToken(data) {
|
|
2500
|
+
async signAccessToken(data, capturedVersion) {
|
|
2393
2501
|
const jti = nanoid4(16);
|
|
2394
|
-
const sessionVersion = await this.getUserSessionVersion(data.userId);
|
|
2502
|
+
const sessionVersion = capturedVersion ?? await this.getUserSessionVersion(data.userId);
|
|
2395
2503
|
const expiresAt = this.expiresAt(this.config.jwt.accessExpiresIn);
|
|
2396
2504
|
if (sessionVersion > 0) {
|
|
2397
2505
|
await this.invalidation.touchSessionVersion(data.userId);
|
|
@@ -2446,7 +2554,7 @@ var TokenService = class TokenService2 {
|
|
|
2446
2554
|
* rebuild the token its rotation issued and hand the browser that one.
|
|
2447
2555
|
*/
|
|
2448
2556
|
successorRefreshToken(replaced, data, expiresAt) {
|
|
2449
|
-
const jti =
|
|
2557
|
+
const jti = createHmac2("sha256", this.config.jwt.refreshSecret).update(`successor:${replaced}`).digest("base64url").slice(0, 16);
|
|
2450
2558
|
const iat = expiresAt - Math.floor(timestring3(this.config.jwt.refreshExpiresIn, "s"));
|
|
2451
2559
|
const token = jwt.sign({ ...data, jti, type: "refresh", iat, exp: expiresAt }, this.config.jwt.refreshSecret);
|
|
2452
2560
|
return { token, expiresAt };
|
|
@@ -2454,7 +2562,8 @@ var TokenService = class TokenService2 {
|
|
|
2454
2562
|
generateRefreshToken(data) {
|
|
2455
2563
|
return this.signRefreshToken({ userId: data.userId, tokenFamily: data.tokenFamily ?? nanoid4(16) }).token;
|
|
2456
2564
|
}
|
|
2457
|
-
async createTokenPair(userId, family, replaced) {
|
|
2565
|
+
async createTokenPair(userId, family, replaced, capturedVersion) {
|
|
2566
|
+
const sessionVersion = capturedVersion ?? await this.getUserSessionVersion(userId);
|
|
2458
2567
|
const { roleName, permissions } = await this.tokenRepository.getRoleAndPermissions(userId);
|
|
2459
2568
|
const accessTokenData = {
|
|
2460
2569
|
userId,
|
|
@@ -2462,7 +2571,8 @@ var TokenService = class TokenService2 {
|
|
|
2462
2571
|
permissions: permissions ?? [],
|
|
2463
2572
|
tokenFamily: family
|
|
2464
2573
|
};
|
|
2465
|
-
|
|
2574
|
+
await this.assertSessionVersion(userId, sessionVersion);
|
|
2575
|
+
const access = await this.signAccessToken(accessTokenData, sessionVersion);
|
|
2466
2576
|
const refresh = replaced ? this.successorRefreshToken(replaced, { userId, tokenFamily: family }, this.expiresAt(this.config.jwt.refreshExpiresIn)) : this.signRefreshToken({ userId, tokenFamily: family });
|
|
2467
2577
|
return {
|
|
2468
2578
|
userId,
|
|
@@ -2476,12 +2586,20 @@ var TokenService = class TokenService2 {
|
|
|
2476
2586
|
refreshTokenExpiresAt: refresh.expiresAt
|
|
2477
2587
|
};
|
|
2478
2588
|
}
|
|
2479
|
-
async generateTokens(userId, tokenFamily) {
|
|
2589
|
+
async generateTokens(userId, tokenFamily, capturedVersion) {
|
|
2480
2590
|
const family = tokenFamily ?? nanoid4(16);
|
|
2481
|
-
const generated = await this.createTokenPair(userId, family);
|
|
2591
|
+
const generated = await this.createTokenPair(userId, family, void 0, capturedVersion);
|
|
2482
2592
|
await this.storeRefreshToken(userId, generated.refreshToken, family);
|
|
2593
|
+
await this.assertSessionVersion(userId, generated.sessionVersion, family);
|
|
2483
2594
|
return generated;
|
|
2484
2595
|
}
|
|
2596
|
+
async assertSessionVersion(userId, expected, issuedFamily) {
|
|
2597
|
+
if (await this.getUserSessionVersion(userId) !== expected) {
|
|
2598
|
+
if (issuedFamily)
|
|
2599
|
+
await this.revokeFamily(issuedFamily);
|
|
2600
|
+
Err7(this.t("errors.tokenRevoked"), 401);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2485
2603
|
// ============ TOKEN BLACKLIST (Cache) ============
|
|
2486
2604
|
/**
|
|
2487
2605
|
* Blacklist an access token by its jti
|
|
@@ -2565,6 +2683,7 @@ var TokenService = class TokenService2 {
|
|
|
2565
2683
|
if (!await this.invalidation.markFamilyIssued(tokenFamily, userId)) {
|
|
2566
2684
|
this.rejectRefreshSession();
|
|
2567
2685
|
}
|
|
2686
|
+
await this.assertSessionVersion(userId, generated.sessionVersion);
|
|
2568
2687
|
return generated;
|
|
2569
2688
|
}
|
|
2570
2689
|
/**
|
|
@@ -2619,12 +2738,15 @@ var TokenService = class TokenService2 {
|
|
|
2619
2738
|
async serveReplacedToken(userId, tokenFamily, replaced, stored) {
|
|
2620
2739
|
const successor = this.successorRefreshToken(replaced, { userId, tokenFamily }, Math.floor(storedTimeMs(stored.expiresAt) / 1e3));
|
|
2621
2740
|
const refresh = this.hashToken(successor.token) === stored.token ? successor : void 0;
|
|
2741
|
+
const sessionVersion = await this.getUserSessionVersion(userId);
|
|
2622
2742
|
const { roleName, permissions } = await this.tokenRepository.getRoleAndPermissions(userId);
|
|
2623
2743
|
const roles = roleName ? [roleName] : [];
|
|
2624
|
-
|
|
2744
|
+
await this.assertSessionVersion(userId, sessionVersion);
|
|
2745
|
+
const access = await this.signAccessToken({ userId, roles, permissions: permissions ?? [], tokenFamily }, sessionVersion);
|
|
2625
2746
|
if (!await this.invalidation.markFamilyIssued(tokenFamily, userId)) {
|
|
2626
2747
|
this.rejectRefreshSession();
|
|
2627
2748
|
}
|
|
2749
|
+
await this.assertSessionVersion(userId, sessionVersion);
|
|
2628
2750
|
return {
|
|
2629
2751
|
userId,
|
|
2630
2752
|
tokenFamily,
|
|
@@ -2652,11 +2774,7 @@ var TokenService = class TokenService2 {
|
|
|
2652
2774
|
}
|
|
2653
2775
|
/** Revoke every refresh session for a user (password change/reset, logout-all). */
|
|
2654
2776
|
async revokeAllForUser(userId) {
|
|
2655
|
-
|
|
2656
|
-
if (Array.isArray(revoked)) {
|
|
2657
|
-
await Promise.all(revoked.map((row) => row?.tokenFamily).filter((family) => typeof family === "string" && !!family).map((family) => this.invalidation.markFamilyRevoked(family)));
|
|
2658
|
-
}
|
|
2659
|
-
return revoked;
|
|
2777
|
+
return this.invalidation.revokeAllForUser(userId);
|
|
2660
2778
|
}
|
|
2661
2779
|
/** Revoke a single refresh session (one family). */
|
|
2662
2780
|
async revokeFamily(tokenFamily) {
|
|
@@ -2771,34 +2889,45 @@ var TokenService = class TokenService2 {
|
|
|
2771
2889
|
* account invites). Stores the jti in cache so the token can only be used
|
|
2772
2890
|
* once, with a TTL matching the JWT expiry.
|
|
2773
2891
|
*/
|
|
2774
|
-
async generateSetPasswordToken(userId, type, expiresIn) {
|
|
2892
|
+
async generateSetPasswordToken(userId, type, expiresIn, expectedEmail) {
|
|
2893
|
+
const user = await this.tokenRepository.getCredentialState(userId);
|
|
2894
|
+
if (!user || expectedEmail !== void 0 && user.email.trim().toLowerCase() !== expectedEmail.trim().toLowerCase()) {
|
|
2895
|
+
Err7(this.t("errors.invalidResetToken"), 401);
|
|
2896
|
+
}
|
|
2897
|
+
const credentialState = credentialFingerprint(this.config.jwt.refreshSecret, user);
|
|
2775
2898
|
const jti = nanoid4(16);
|
|
2776
2899
|
const data = {
|
|
2777
2900
|
userId,
|
|
2778
2901
|
type,
|
|
2779
2902
|
jti,
|
|
2780
|
-
timestamp: Date.now()
|
|
2903
|
+
timestamp: Date.now(),
|
|
2904
|
+
credentialState
|
|
2781
2905
|
};
|
|
2782
2906
|
const token = jwt.sign(data, this.config.jwt.refreshSecret, {
|
|
2783
2907
|
expiresIn
|
|
2784
2908
|
});
|
|
2785
2909
|
await this.cache.set(`${this.resetTokenPrefix}${userId}`, jti, timestring3(expiresIn, "ms"));
|
|
2910
|
+
const current = await this.tokenRepository.getCredentialState(userId);
|
|
2911
|
+
if (!current || credentialFingerprint(this.config.jwt.refreshSecret, current) !== credentialState) {
|
|
2912
|
+
await this.discardSetPasswordToken(userId, jti);
|
|
2913
|
+
Err7(this.t("errors.invalidResetToken"), 401);
|
|
2914
|
+
}
|
|
2786
2915
|
return { token, userId, jti };
|
|
2787
2916
|
}
|
|
2788
2917
|
/**
|
|
2789
2918
|
* Generate secure password reset token
|
|
2790
2919
|
* Returns both the plain token (to send via email) and userId for identification
|
|
2791
2920
|
*/
|
|
2792
|
-
async generateResetToken(userId) {
|
|
2793
|
-
return this.generateSetPasswordToken(userId, "reset", "1h");
|
|
2921
|
+
async generateResetToken(userId, expectedEmail) {
|
|
2922
|
+
return this.generateSetPasswordToken(userId, "reset", "1h", expectedEmail);
|
|
2794
2923
|
}
|
|
2795
2924
|
/**
|
|
2796
2925
|
* Generate secure account-invite token.
|
|
2797
2926
|
* Longer expiry (3d) than reset because an invited user may not check
|
|
2798
2927
|
* their email immediately. Consumed via the same reset-password endpoint.
|
|
2799
2928
|
*/
|
|
2800
|
-
async generateInviteToken(userId) {
|
|
2801
|
-
return this.generateSetPasswordToken(userId, "invite", "3d");
|
|
2929
|
+
async generateInviteToken(userId, expectedEmail) {
|
|
2930
|
+
return this.generateSetPasswordToken(userId, "invite", "3d", expectedEmail);
|
|
2802
2931
|
}
|
|
2803
2932
|
/**
|
|
2804
2933
|
* Discard a set-password token this process just minted, identified by the
|
|
@@ -2836,11 +2965,11 @@ var TokenService = class TokenService2 {
|
|
|
2836
2965
|
async consumeSetPasswordToken(token) {
|
|
2837
2966
|
let decoded;
|
|
2838
2967
|
try {
|
|
2839
|
-
decoded = jwt.verify(token, this.config.jwt.refreshSecret);
|
|
2968
|
+
decoded = jwt.verify(token, this.config.jwt.refreshSecret, { algorithms: ["HS256"] });
|
|
2840
2969
|
} catch {
|
|
2841
2970
|
Err7(this.t("errors.resetTokenExpired"));
|
|
2842
2971
|
}
|
|
2843
|
-
if (decoded.type !== "reset" && decoded.type !== "invite" || !decoded.jti) {
|
|
2972
|
+
if (!decoded || typeof decoded !== "object" || decoded.type !== "reset" && decoded.type !== "invite" || typeof decoded.userId !== "string" || !decoded.userId || typeof decoded.jti !== "string" || !decoded.jti || typeof decoded.credentialState !== "string" || !decoded.credentialState || !Number.isFinite(decoded.exp)) {
|
|
2844
2973
|
Err7(this.t("errors.invalidResetToken"));
|
|
2845
2974
|
}
|
|
2846
2975
|
const consume = this.cache.compareAndDelete;
|
|
@@ -2848,12 +2977,21 @@ var TokenService = class TokenService2 {
|
|
|
2848
2977
|
Err7.invalidOperation("Password reset requires a cache with atomic compare-and-delete");
|
|
2849
2978
|
}
|
|
2850
2979
|
const key = `${this.resetTokenPrefix}${decoded.userId}`;
|
|
2980
|
+
const matches = /* @__PURE__ */ __name(async () => {
|
|
2981
|
+
const current = await this.tokenRepository.getCredentialState(decoded.userId);
|
|
2982
|
+
return current && credentialFingerprint(this.config.jwt.refreshSecret, current) === decoded.credentialState;
|
|
2983
|
+
}, "matches");
|
|
2984
|
+
if (!await matches())
|
|
2985
|
+
Err7(this.t("errors.invalidResetToken"));
|
|
2851
2986
|
if (!await consume.call(this.cache, key, decoded.jti)) {
|
|
2852
2987
|
Err7(this.t("errors.invalidResetToken"));
|
|
2853
2988
|
}
|
|
2989
|
+
if (!await matches())
|
|
2990
|
+
Err7(this.t("errors.invalidResetToken"));
|
|
2854
2991
|
return {
|
|
2855
2992
|
userId: decoded.userId,
|
|
2856
|
-
type: decoded.type
|
|
2993
|
+
type: decoded.type,
|
|
2994
|
+
credentialState: decoded.credentialState
|
|
2857
2995
|
};
|
|
2858
2996
|
}
|
|
2859
2997
|
/**
|
|
@@ -2878,7 +3016,7 @@ __decorate12([
|
|
|
2878
3016
|
], TokenService.prototype, "t", void 0);
|
|
2879
3017
|
TokenService = TokenService_1 = __decorate12([
|
|
2880
3018
|
Injectable7(),
|
|
2881
|
-
__metadata12("design:paramtypes", [typeof (_a7 = typeof TokenRepository !== "undefined" && TokenRepository) === "function" ? _a7 : Object, typeof (_b5 = typeof CookieManager !== "undefined" && CookieManager) === "function" ? _b5 : Object, typeof (
|
|
3019
|
+
__metadata12("design:paramtypes", [typeof (_a7 = typeof TokenRepository !== "undefined" && TokenRepository) === "function" ? _a7 : Object, typeof (_b5 = typeof CookieManager !== "undefined" && CookieManager) === "function" ? _b5 : Object, typeof (_c4 = typeof CacheService2 !== "undefined" && CacheService2) === "function" ? _c4 : Object, typeof (_d3 = typeof CredentialSetupRequirementRepository !== "undefined" && CredentialSetupRequirementRepository) === "function" ? _d3 : Object, typeof (_e2 = typeof SessionInvalidationService !== "undefined" && SessionInvalidationService) === "function" ? _e2 : Object])
|
|
2882
3020
|
], TokenService);
|
|
2883
3021
|
|
|
2884
3022
|
// src/auth/AuthService.ts
|
|
@@ -2972,7 +3110,7 @@ var __metadata13 = function(k, v) {
|
|
|
2972
3110
|
};
|
|
2973
3111
|
var _a8;
|
|
2974
3112
|
var _b6;
|
|
2975
|
-
var
|
|
3113
|
+
var _c5;
|
|
2976
3114
|
var CredentialSetupRequirementService = class CredentialSetupRequirementService2 {
|
|
2977
3115
|
static {
|
|
2978
3116
|
__name(this, "CredentialSetupRequirementService");
|
|
@@ -3015,7 +3153,7 @@ __decorate13([
|
|
|
3015
3153
|
Transaction2({ retries: 2 }),
|
|
3016
3154
|
__metadata13("design:type", Function),
|
|
3017
3155
|
__metadata13("design:paramtypes", [String, String, Object]),
|
|
3018
|
-
__metadata13("design:returntype", typeof (
|
|
3156
|
+
__metadata13("design:returntype", typeof (_c5 = typeof Promise !== "undefined" && Promise) === "function" ? _c5 : Object)
|
|
3019
3157
|
], CredentialSetupRequirementService.prototype, "markRequired", null);
|
|
3020
3158
|
CredentialSetupRequirementService = __decorate13([
|
|
3021
3159
|
Injectable8(),
|
|
@@ -3044,6 +3182,9 @@ var credentialSetupError = /* @__PURE__ */ __name((code, message, status = 400)
|
|
|
3044
3182
|
throw new BaseError(code, message, status);
|
|
3045
3183
|
}, "credentialSetupError");
|
|
3046
3184
|
|
|
3185
|
+
// src/auth/verifiedCredential.ts
|
|
3186
|
+
var verifiedCredentials = /* @__PURE__ */ new WeakMap();
|
|
3187
|
+
|
|
3047
3188
|
// src/auth/AuthSessionService.ts
|
|
3048
3189
|
var __decorate14 = function(decorators, target, key, desc) {
|
|
3049
3190
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -3056,8 +3197,8 @@ var __metadata14 = function(k, v) {
|
|
|
3056
3197
|
};
|
|
3057
3198
|
var _a9;
|
|
3058
3199
|
var _b7;
|
|
3059
|
-
var
|
|
3060
|
-
var
|
|
3200
|
+
var _c6;
|
|
3201
|
+
var _d4;
|
|
3061
3202
|
var AuthSessionService = class AuthSessionService2 {
|
|
3062
3203
|
static {
|
|
3063
3204
|
__name(this, "AuthSessionService");
|
|
@@ -3073,17 +3214,28 @@ var AuthSessionService = class AuthSessionService2 {
|
|
|
3073
3214
|
this.credentialSetupRequirements = credentialSetupRequirements;
|
|
3074
3215
|
}
|
|
3075
3216
|
async establish(user) {
|
|
3076
|
-
|
|
3217
|
+
const sessionVersion = await this.tokenService.getSessionVersion(user.id);
|
|
3218
|
+
const current = await this.userService.getAuthRecordById(user.id);
|
|
3219
|
+
if (!current || current.status !== "active" || user.status !== "active") {
|
|
3077
3220
|
Err9("oauth_account_inactive", 403);
|
|
3078
3221
|
}
|
|
3222
|
+
const verifiedHash = verifiedCredentials.get(user);
|
|
3223
|
+
if (verifiedHash !== void 0 && verifiedHash !== current.password || ["email", "phone", "emailVerified", "roleId"].some((field) => field in user && (user[field] ?? null) !== (current[field] ?? null))) {
|
|
3224
|
+
Err9("errors.invalidCredentials", 401);
|
|
3225
|
+
}
|
|
3079
3226
|
if (await this.credentialSetupRequirements?.isRequired(user.id, PASSWORD_SETUP_PURPOSE)) {
|
|
3080
3227
|
credentialSetupError(CREDENTIAL_SETUP_CODES.REQUIRED, "Credential setup is required before a session can be established", 403);
|
|
3081
3228
|
}
|
|
3082
3229
|
await this.tokenService.deleteExpiredSessions();
|
|
3083
|
-
const generated = await this.tokenService.generateTokens(user.id);
|
|
3084
|
-
this.cookieManager.setRefreshToken(generated.refreshToken);
|
|
3230
|
+
const generated = await this.tokenService.generateTokens(user.id, void 0, sessionVersion);
|
|
3085
3231
|
await this.userService.updateLastLogin(user.id);
|
|
3086
|
-
|
|
3232
|
+
if (await this.tokenService.getSessionVersion(user.id) !== generated.sessionVersion) {
|
|
3233
|
+
await this.tokenService.revokeFamily(generated.tokenFamily);
|
|
3234
|
+
Err9("errors.tokenRevoked", 401);
|
|
3235
|
+
}
|
|
3236
|
+
this.cookieManager.setRefreshToken(generated.refreshToken);
|
|
3237
|
+
const { roles, permissions, tokenFamily } = generated;
|
|
3238
|
+
user = { ...user, role: roles[0] ?? null };
|
|
3087
3239
|
this.cookieManager.setSessionCookie({
|
|
3088
3240
|
user: {
|
|
3089
3241
|
id: user.id,
|
|
@@ -3103,22 +3255,22 @@ var AuthSessionService = class AuthSessionService2 {
|
|
|
3103
3255
|
};
|
|
3104
3256
|
AuthSessionService = __decorate14([
|
|
3105
3257
|
Injectable9(),
|
|
3106
|
-
__metadata14("design:paramtypes", [typeof (_a9 = typeof TokenService !== "undefined" && TokenService) === "function" ? _a9 : Object, typeof (_b7 = typeof UserService !== "undefined" && UserService) === "function" ? _b7 : Object, typeof (
|
|
3258
|
+
__metadata14("design:paramtypes", [typeof (_a9 = typeof TokenService !== "undefined" && TokenService) === "function" ? _a9 : Object, typeof (_b7 = typeof UserService !== "undefined" && UserService) === "function" ? _b7 : Object, typeof (_c6 = typeof CookieManager !== "undefined" && CookieManager) === "function" ? _c6 : Object, typeof (_d4 = typeof CredentialSetupRequirementService !== "undefined" && CredentialSetupRequirementService) === "function" ? _d4 : Object])
|
|
3107
3259
|
], AuthSessionService);
|
|
3108
3260
|
|
|
3109
3261
|
// src/credentialSetup/PasswordSetupService.ts
|
|
3110
|
-
import { Inject as
|
|
3262
|
+
import { Inject as Inject12, Injectable as Injectable11 } from "najm-core";
|
|
3111
3263
|
import { I18n as I18n7 } from "najm-i18n";
|
|
3112
3264
|
|
|
3113
3265
|
// src/credentialSetup/CredentialSetupService.ts
|
|
3114
3266
|
import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
3115
3267
|
import { CookieService as CookieService2 } from "najm-cookies";
|
|
3116
|
-
import { Err as Err11, Injectable as Injectable10 } from "najm-core";
|
|
3268
|
+
import { Err as Err11, Inject as Inject11, Injectable as Injectable10 } from "najm-core";
|
|
3117
3269
|
import { Transaction as Transaction3 } from "najm-database";
|
|
3118
3270
|
import { I18n as I18n6 } from "najm-i18n";
|
|
3119
3271
|
|
|
3120
3272
|
// src/credentialSetup/CredentialSetupRepository.ts
|
|
3121
|
-
import { and as
|
|
3273
|
+
import { and as and4, eq as eq6, gt, isNull as isNull3, lt as lt2 } from "drizzle-orm";
|
|
3122
3274
|
import { Err as Err10, Inject as Inject10, Repository as Repository5 } from "najm-core";
|
|
3123
3275
|
import { DB as DB5 } from "najm-database";
|
|
3124
3276
|
var __decorate15 = function(decorators, target, key, desc) {
|
|
@@ -3145,7 +3297,7 @@ var CredentialSetupRepository = class CredentialSetupRepository2 {
|
|
|
3145
3297
|
}
|
|
3146
3298
|
async replaceActive(data) {
|
|
3147
3299
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3148
|
-
await this.db.update(this.sessions).set({ revokedAt: now, updatedAt: now }).where(
|
|
3300
|
+
await this.db.update(this.sessions).set({ revokedAt: now, updatedAt: now }).where(and4(eq6(this.sessions.userId, data.userId), eq6(this.sessions.purpose, data.purpose), isNull3(this.sessions.consumedAt), isNull3(this.sessions.revokedAt)));
|
|
3149
3301
|
const [session] = await this.db.insert(this.sessions).values(data).returning({
|
|
3150
3302
|
userId: this.sessions.userId,
|
|
3151
3303
|
purpose: this.sessions.purpose,
|
|
@@ -3158,12 +3310,12 @@ var CredentialSetupRepository = class CredentialSetupRepository2 {
|
|
|
3158
3310
|
userId: this.sessions.userId,
|
|
3159
3311
|
purpose: this.sessions.purpose,
|
|
3160
3312
|
expiresAt: this.sessions.expiresAt
|
|
3161
|
-
}).from(this.sessions).where(
|
|
3313
|
+
}).from(this.sessions).where(and4(eq6(this.sessions.tokenHash, tokenHash), eq6(this.sessions.purpose, purpose), isNull3(this.sessions.consumedAt), isNull3(this.sessions.revokedAt), gt(this.sessions.expiresAt, (/* @__PURE__ */ new Date()).toISOString()))).limit(1);
|
|
3162
3314
|
return session;
|
|
3163
3315
|
}
|
|
3164
3316
|
async consume(tokenHash, purpose) {
|
|
3165
3317
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3166
|
-
const [session] = await this.db.update(this.sessions).set({ consumedAt: now, updatedAt: now }).where(
|
|
3318
|
+
const [session] = await this.db.update(this.sessions).set({ consumedAt: now, updatedAt: now }).where(and4(eq6(this.sessions.tokenHash, tokenHash), eq6(this.sessions.purpose, purpose), isNull3(this.sessions.consumedAt), isNull3(this.sessions.revokedAt), gt(this.sessions.expiresAt, now))).returning({
|
|
3167
3319
|
userId: this.sessions.userId,
|
|
3168
3320
|
purpose: this.sessions.purpose,
|
|
3169
3321
|
expiresAt: this.sessions.expiresAt
|
|
@@ -3172,7 +3324,7 @@ var CredentialSetupRepository = class CredentialSetupRepository2 {
|
|
|
3172
3324
|
}
|
|
3173
3325
|
async revoke(tokenHash, purpose) {
|
|
3174
3326
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3175
|
-
const [session] = await this.db.update(this.sessions).set({ revokedAt: now, updatedAt: now }).where(
|
|
3327
|
+
const [session] = await this.db.update(this.sessions).set({ revokedAt: now, updatedAt: now }).where(and4(eq6(this.sessions.tokenHash, tokenHash), eq6(this.sessions.purpose, purpose), isNull3(this.sessions.consumedAt), isNull3(this.sessions.revokedAt))).returning({ userId: this.sessions.userId });
|
|
3176
3328
|
return session;
|
|
3177
3329
|
}
|
|
3178
3330
|
async deleteExpired() {
|
|
@@ -3203,11 +3355,12 @@ var __metadata16 = function(k, v) {
|
|
|
3203
3355
|
};
|
|
3204
3356
|
var _a10;
|
|
3205
3357
|
var _b8;
|
|
3206
|
-
var
|
|
3207
|
-
var
|
|
3358
|
+
var _c7;
|
|
3359
|
+
var _d5;
|
|
3208
3360
|
var _e3;
|
|
3209
3361
|
var _f2;
|
|
3210
3362
|
var _g2;
|
|
3363
|
+
var _h2;
|
|
3211
3364
|
var DEFAULT_CREDENTIAL_SETUP_COOKIE_NAME = "najm.credential-setup";
|
|
3212
3365
|
var DEFAULT_CREDENTIAL_SETUP_TTL_MS = 10 * 60 * 1e3;
|
|
3213
3366
|
var MAX_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -3220,12 +3373,15 @@ var CredentialSetupService = class CredentialSetupService2 {
|
|
|
3220
3373
|
tokens;
|
|
3221
3374
|
authCookies;
|
|
3222
3375
|
cookies;
|
|
3376
|
+
users;
|
|
3223
3377
|
t;
|
|
3224
|
-
|
|
3378
|
+
config;
|
|
3379
|
+
constructor(repository, tokens, authCookies, cookies2, users) {
|
|
3225
3380
|
this.repository = repository;
|
|
3226
3381
|
this.tokens = tokens;
|
|
3227
3382
|
this.authCookies = authCookies;
|
|
3228
3383
|
this.cookies = cookies2;
|
|
3384
|
+
this.users = users;
|
|
3229
3385
|
}
|
|
3230
3386
|
/**
|
|
3231
3387
|
* Revoke normal auth sessions and issue a single-purpose, short-lived,
|
|
@@ -3233,7 +3389,10 @@ var CredentialSetupService = class CredentialSetupService2 {
|
|
|
3233
3389
|
*/
|
|
3234
3390
|
async begin(userId, options) {
|
|
3235
3391
|
const resolved = this.resolveOptions(options);
|
|
3236
|
-
const
|
|
3392
|
+
const user = await this.users.getRawById(userId);
|
|
3393
|
+
if (!user)
|
|
3394
|
+
this.sessionInvalid();
|
|
3395
|
+
const token = `${randomBytes2(32).toString("base64url")}.${credentialFingerprint(this.config.jwt.refreshSecret, user)}`;
|
|
3237
3396
|
const expiresAt = new Date(Date.now() + resolved.ttlMs).toISOString();
|
|
3238
3397
|
await this.repository.deleteExpired();
|
|
3239
3398
|
await this.repository.replaceActive({
|
|
@@ -3244,6 +3403,7 @@ var CredentialSetupService = class CredentialSetupService2 {
|
|
|
3244
3403
|
});
|
|
3245
3404
|
await this.tokens.invalidateUserAccessTokens(userId);
|
|
3246
3405
|
await this.tokens.revokeAllForUser(userId);
|
|
3406
|
+
await this.requireCurrentCredential(userId, token, resolved);
|
|
3247
3407
|
this.authCookies.clearRefreshToken();
|
|
3248
3408
|
this.authCookies.clearSessionCookie();
|
|
3249
3409
|
this.setCookie(token, resolved);
|
|
@@ -3260,6 +3420,7 @@ var CredentialSetupService = class CredentialSetupService2 {
|
|
|
3260
3420
|
this.clearCookie(resolved);
|
|
3261
3421
|
this.sessionInvalid();
|
|
3262
3422
|
}
|
|
3423
|
+
await this.requireCurrentCredential(session.userId, token, resolved);
|
|
3263
3424
|
return session;
|
|
3264
3425
|
}
|
|
3265
3426
|
/**
|
|
@@ -3277,10 +3438,19 @@ var CredentialSetupService = class CredentialSetupService2 {
|
|
|
3277
3438
|
this.clearCookie(resolved);
|
|
3278
3439
|
this.sessionInvalid();
|
|
3279
3440
|
}
|
|
3441
|
+
await this.requireCurrentCredential(session.userId, token, resolved);
|
|
3280
3442
|
const result = await complete(session);
|
|
3281
3443
|
this.clearCookie(resolved);
|
|
3282
3444
|
return result;
|
|
3283
3445
|
}
|
|
3446
|
+
async requireCurrentCredential(userId, token, options) {
|
|
3447
|
+
const fingerprint = token.split(".")[1];
|
|
3448
|
+
const user = await this.users.getRawById(userId);
|
|
3449
|
+
if (!fingerprint || !user || credentialFingerprint(this.config.jwt.refreshSecret, user) !== fingerprint) {
|
|
3450
|
+
this.clearCookie(options);
|
|
3451
|
+
this.sessionInvalid();
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3284
3454
|
async cancel(options) {
|
|
3285
3455
|
const resolved = this.resolveOptions(options);
|
|
3286
3456
|
const token = this.cookies.get(resolved.cookieName);
|
|
@@ -3345,27 +3515,31 @@ __decorate16([
|
|
|
3345
3515
|
I18n6("auth"),
|
|
3346
3516
|
__metadata16("design:type", Object)
|
|
3347
3517
|
], CredentialSetupService.prototype, "t", void 0);
|
|
3518
|
+
__decorate16([
|
|
3519
|
+
Inject11(AUTH_CONFIG),
|
|
3520
|
+
__metadata16("design:type", Object)
|
|
3521
|
+
], CredentialSetupService.prototype, "config", void 0);
|
|
3348
3522
|
__decorate16([
|
|
3349
3523
|
Transaction3({ retries: 2 }),
|
|
3350
3524
|
__metadata16("design:type", Function),
|
|
3351
3525
|
__metadata16("design:paramtypes", [String, Object]),
|
|
3352
|
-
__metadata16("design:returntype", typeof (
|
|
3526
|
+
__metadata16("design:returntype", typeof (_f2 = typeof Promise !== "undefined" && Promise) === "function" ? _f2 : Object)
|
|
3353
3527
|
], CredentialSetupService.prototype, "begin", null);
|
|
3354
3528
|
__decorate16([
|
|
3355
3529
|
Transaction3({ retries: 2 }),
|
|
3356
3530
|
__metadata16("design:type", Function),
|
|
3357
3531
|
__metadata16("design:paramtypes", [Object, Function]),
|
|
3358
|
-
__metadata16("design:returntype", typeof (
|
|
3532
|
+
__metadata16("design:returntype", typeof (_g2 = typeof Promise !== "undefined" && Promise) === "function" ? _g2 : Object)
|
|
3359
3533
|
], CredentialSetupService.prototype, "consume", null);
|
|
3360
3534
|
__decorate16([
|
|
3361
3535
|
Transaction3({ retries: 2 }),
|
|
3362
3536
|
__metadata16("design:type", Function),
|
|
3363
3537
|
__metadata16("design:paramtypes", [Object]),
|
|
3364
|
-
__metadata16("design:returntype", typeof (
|
|
3538
|
+
__metadata16("design:returntype", typeof (_h2 = typeof Promise !== "undefined" && Promise) === "function" ? _h2 : Object)
|
|
3365
3539
|
], CredentialSetupService.prototype, "cancel", null);
|
|
3366
3540
|
CredentialSetupService = __decorate16([
|
|
3367
3541
|
Injectable10(),
|
|
3368
|
-
__metadata16("design:paramtypes", [typeof (_a10 = typeof CredentialSetupRepository !== "undefined" && CredentialSetupRepository) === "function" ? _a10 : Object, typeof (_b8 = typeof TokenService !== "undefined" && TokenService) === "function" ? _b8 : Object, typeof (
|
|
3542
|
+
__metadata16("design:paramtypes", [typeof (_a10 = typeof CredentialSetupRepository !== "undefined" && CredentialSetupRepository) === "function" ? _a10 : Object, typeof (_b8 = typeof TokenService !== "undefined" && TokenService) === "function" ? _b8 : Object, typeof (_c7 = typeof CookieManager !== "undefined" && CookieManager) === "function" ? _c7 : Object, typeof (_d5 = typeof CookieService2 !== "undefined" && CookieService2) === "function" ? _d5 : Object, typeof (_e3 = typeof UserRepository !== "undefined" && UserRepository) === "function" ? _e3 : Object])
|
|
3369
3543
|
], CredentialSetupService);
|
|
3370
3544
|
|
|
3371
3545
|
// src/credentialSetup/PasswordSetupService.ts
|
|
@@ -3380,8 +3554,8 @@ var __metadata17 = function(k, v) {
|
|
|
3380
3554
|
};
|
|
3381
3555
|
var _a11;
|
|
3382
3556
|
var _b9;
|
|
3383
|
-
var
|
|
3384
|
-
var
|
|
3557
|
+
var _c8;
|
|
3558
|
+
var _d6;
|
|
3385
3559
|
var _e4;
|
|
3386
3560
|
var _f3;
|
|
3387
3561
|
var PasswordSetupService = class PasswordSetupService2 {
|
|
@@ -3440,9 +3614,9 @@ var PasswordSetupService = class PasswordSetupService2 {
|
|
|
3440
3614
|
*/
|
|
3441
3615
|
async change(newPassword) {
|
|
3442
3616
|
const { userId } = await this.setup.require(this.options);
|
|
3443
|
-
const password = await this.validateReplacement(userId, newPassword);
|
|
3617
|
+
const { password, credential } = await this.validateReplacement(userId, newPassword);
|
|
3444
3618
|
const hashed = await this.encryption.hashPassword(password);
|
|
3445
|
-
await this.setup.consume(this.options, (session) => this.persist(session.userId, hashed));
|
|
3619
|
+
await this.setup.consume(this.options, (session) => this.persist(session.userId, hashed, credential));
|
|
3446
3620
|
return { changed: true, signInAgain: true };
|
|
3447
3621
|
}
|
|
3448
3622
|
async cancel() {
|
|
@@ -3465,15 +3639,18 @@ var PasswordSetupService = class PasswordSetupService2 {
|
|
|
3465
3639
|
if (await this.isCurrentPassword(newPassword, kind.normalize(newPassword), user.password)) {
|
|
3466
3640
|
credentialSetupError(CREDENTIAL_SETUP_CODES.SAME_PASSWORD, this.message("errors.credentialSetupSamePassword", "Choose a password different from your current one."), 400);
|
|
3467
3641
|
}
|
|
3468
|
-
return newPassword;
|
|
3642
|
+
return { password: newPassword, credential: user };
|
|
3469
3643
|
}
|
|
3470
3644
|
/**
|
|
3471
3645
|
* Runs inside the setup-session consumption transaction, so the password
|
|
3472
3646
|
* update, the requirement completion, and the one-time session all commit or
|
|
3473
3647
|
* roll back together.
|
|
3474
3648
|
*/
|
|
3475
|
-
async persist(userId, password) {
|
|
3476
|
-
|
|
3649
|
+
async persist(userId, password, credential) {
|
|
3650
|
+
if (userId !== credential.id)
|
|
3651
|
+
this.validator.checkCredentialUnchanged(void 0);
|
|
3652
|
+
const updated = await this.userRecords.updateWithCredential(userId, { password }, credential);
|
|
3653
|
+
this.validator.checkCredentialUnchanged(updated);
|
|
3477
3654
|
const completed = await this.requirements.completeRequirement(userId, PASSWORD_SETUP_PURPOSE);
|
|
3478
3655
|
if (!completed) {
|
|
3479
3656
|
credentialSetupError(CREDENTIAL_SETUP_CODES.ALREADY_COMPLETED, this.message("errors.credentialSetupAlreadyCompleted", "The password was already replaced. Please sign in again."), 409);
|
|
@@ -3485,6 +3662,9 @@ var PasswordSetupService = class PasswordSetupService2 {
|
|
|
3485
3662
|
if (!parsed.success) {
|
|
3486
3663
|
credentialSetupError(CREDENTIAL_SETUP_CODES.PASSWORD_REJECTED, parsed.error.issues[0]?.message ?? this.message("errors.credentialSetupPasswordRejected", "Choose a different password."), 422);
|
|
3487
3664
|
}
|
|
3665
|
+
if (!parsed.data || new TextEncoder().encode(parsed.data).length > 72) {
|
|
3666
|
+
credentialSetupError(CREDENTIAL_SETUP_CODES.PASSWORD_REJECTED, "Password must be between 1 and 72 bytes", 422);
|
|
3667
|
+
}
|
|
3488
3668
|
return parsed.data;
|
|
3489
3669
|
}
|
|
3490
3670
|
/**
|
|
@@ -3500,7 +3680,7 @@ var PasswordSetupService = class PasswordSetupService2 {
|
|
|
3500
3680
|
}
|
|
3501
3681
|
};
|
|
3502
3682
|
__decorate17([
|
|
3503
|
-
|
|
3683
|
+
Inject12(AUTH_CONFIG),
|
|
3504
3684
|
__metadata17("design:type", Object)
|
|
3505
3685
|
], PasswordSetupService.prototype, "config", void 0);
|
|
3506
3686
|
__decorate17([
|
|
@@ -3509,7 +3689,7 @@ __decorate17([
|
|
|
3509
3689
|
], PasswordSetupService.prototype, "t", void 0);
|
|
3510
3690
|
PasswordSetupService = __decorate17([
|
|
3511
3691
|
Injectable11(),
|
|
3512
|
-
__metadata17("design:paramtypes", [typeof (_a11 = typeof CredentialSetupService !== "undefined" && CredentialSetupService) === "function" ? _a11 : Object, typeof (_b9 = typeof CredentialSetupRequirementService !== "undefined" && CredentialSetupRequirementService) === "function" ? _b9 : Object, typeof (
|
|
3692
|
+
__metadata17("design:paramtypes", [typeof (_a11 = typeof CredentialSetupService !== "undefined" && CredentialSetupService) === "function" ? _a11 : Object, typeof (_b9 = typeof CredentialSetupRequirementService !== "undefined" && CredentialSetupRequirementService) === "function" ? _b9 : Object, typeof (_c8 = typeof UserService !== "undefined" && UserService) === "function" ? _c8 : Object, typeof (_d6 = typeof UserRepository !== "undefined" && UserRepository) === "function" ? _d6 : Object, typeof (_e4 = typeof UserValidator !== "undefined" && UserValidator) === "function" ? _e4 : Object, typeof (_f3 = typeof EncryptionService !== "undefined" && EncryptionService) === "function" ? _f3 : Object])
|
|
3513
3693
|
], PasswordSetupService);
|
|
3514
3694
|
|
|
3515
3695
|
// src/auth/AuthService.ts
|
|
@@ -3524,12 +3704,12 @@ var __metadata18 = function(k, v) {
|
|
|
3524
3704
|
};
|
|
3525
3705
|
var _a12;
|
|
3526
3706
|
var _b10;
|
|
3527
|
-
var
|
|
3528
|
-
var
|
|
3707
|
+
var _c9;
|
|
3708
|
+
var _d7;
|
|
3529
3709
|
var _e5;
|
|
3530
3710
|
var _f4;
|
|
3531
3711
|
var _g3;
|
|
3532
|
-
var
|
|
3712
|
+
var _h3;
|
|
3533
3713
|
var _j2;
|
|
3534
3714
|
var _k;
|
|
3535
3715
|
var _l;
|
|
@@ -3623,7 +3803,7 @@ var AuthService = class AuthService2 {
|
|
|
3623
3803
|
* Nothing here logs the token, the link, the message body, or the recipient.
|
|
3624
3804
|
*/
|
|
3625
3805
|
async deliverInvitation(userId, email2, userName, role) {
|
|
3626
|
-
const { token, jti } = await this.tokenService.generateInviteToken(userId);
|
|
3806
|
+
const { token, jti } = await this.tokenService.generateInviteToken(userId, email2);
|
|
3627
3807
|
const inviteLink = `${this.config.frontendUrl}/reset-password?token=${token}`;
|
|
3628
3808
|
const accountType = role?.trim().toLowerCase() || void 0;
|
|
3629
3809
|
const accountLabel = accountType ? `${accountType} account` : "account";
|
|
@@ -3817,6 +3997,7 @@ var AuthService = class AuthService2 {
|
|
|
3817
3997
|
await this.userService.resetFailedAttempts(user.id);
|
|
3818
3998
|
}
|
|
3819
3999
|
const { password: _, failedLoginAttempts: __, lockoutUntil: ___, ...sanitized } = user;
|
|
4000
|
+
verifiedCredentials.set(sanitized, storedHash);
|
|
3820
4001
|
return { user: sanitized, requirement };
|
|
3821
4002
|
}
|
|
3822
4003
|
/**
|
|
@@ -3956,12 +4137,12 @@ var AuthService = class AuthService2 {
|
|
|
3956
4137
|
return result;
|
|
3957
4138
|
}
|
|
3958
4139
|
async forgotPassword(email2) {
|
|
3959
|
-
const user = await this.userService.
|
|
4140
|
+
const user = await this.userService.findByEmailInsensitive(email2);
|
|
3960
4141
|
if (user) {
|
|
3961
|
-
const { token } = await this.tokenService.generateResetToken(user.id);
|
|
3962
|
-
const resetLink = `${this.config.frontendUrl}/reset-password?token=${token}`;
|
|
3963
4142
|
try {
|
|
3964
|
-
await this.
|
|
4143
|
+
const { token } = await this.tokenService.generateResetToken(user.id, user.email);
|
|
4144
|
+
const resetLink = `${this.config.frontendUrl}/reset-password?token=${token}`;
|
|
4145
|
+
await this.emailService.sendHtml(user.email, this.t("emails.passwordReset.subject"), passwordResetTemplate({
|
|
3965
4146
|
resetLink,
|
|
3966
4147
|
userName: user.name || email2
|
|
3967
4148
|
}));
|
|
@@ -3981,7 +4162,7 @@ var AuthService = class AuthService2 {
|
|
|
3981
4162
|
Err12(this.t("errors.invalidCredentials"), 401);
|
|
3982
4163
|
}
|
|
3983
4164
|
this.userValidator.validatePasswordStrength(newPassword);
|
|
3984
|
-
await this.userService.update(userId, { password: newPassword });
|
|
4165
|
+
await this.userService.update(userId, { password: newPassword }, { expectedCredential: user });
|
|
3985
4166
|
await this.tokenService.invalidateUserAccessTokens(userId);
|
|
3986
4167
|
await this.tokenService.revokeAllForUser(userId);
|
|
3987
4168
|
this.cookieManager.clearRefreshToken();
|
|
@@ -3991,7 +4172,10 @@ var AuthService = class AuthService2 {
|
|
|
3991
4172
|
async resetPassword(token, newPassword) {
|
|
3992
4173
|
this.userValidator.validatePasswordStrength(newPassword);
|
|
3993
4174
|
const consumed = await this.tokenService.consumeSetPasswordToken(token);
|
|
3994
|
-
const user = await this.userService.
|
|
4175
|
+
const user = await this.userService.getAuthRecordById(consumed.userId);
|
|
4176
|
+
if (!user || credentialFingerprint(this.config.jwt.refreshSecret, user) !== consumed.credentialState) {
|
|
4177
|
+
Err12(this.t("errors.invalidResetToken"), 401);
|
|
4178
|
+
}
|
|
3995
4179
|
const acceptsInvitation = consumed.type === "invite";
|
|
3996
4180
|
await this.userService.update(consumed.userId, {
|
|
3997
4181
|
password: newPassword,
|
|
@@ -3999,7 +4183,7 @@ var AuthService = class AuthService2 {
|
|
|
3999
4183
|
emailVerified: true,
|
|
4000
4184
|
...user.status === "pending" ? { status: "active" } : {}
|
|
4001
4185
|
} : {}
|
|
4002
|
-
});
|
|
4186
|
+
}, { expectedCredential: user });
|
|
4003
4187
|
await this.tokenService.invalidateUserAccessTokens(consumed.userId);
|
|
4004
4188
|
await this.tokenService.revokeAllForUser(consumed.userId);
|
|
4005
4189
|
this.cookieManager.clearRefreshToken();
|
|
@@ -4075,7 +4259,7 @@ var AuthService = class AuthService2 {
|
|
|
4075
4259
|
if (!email2) {
|
|
4076
4260
|
Err12("This account has no email address to send a password reset to", 409);
|
|
4077
4261
|
}
|
|
4078
|
-
const { token, jti } = await this.tokenService.generateResetToken(user.id);
|
|
4262
|
+
const { token, jti } = await this.tokenService.generateResetToken(user.id, user.email);
|
|
4079
4263
|
const resetLink = `${this.config.frontendUrl}/reset-password?token=${token}`;
|
|
4080
4264
|
let emailSent = false;
|
|
4081
4265
|
try {
|
|
@@ -4137,7 +4321,7 @@ var AuthService = class AuthService2 {
|
|
|
4137
4321
|
}
|
|
4138
4322
|
};
|
|
4139
4323
|
__decorate18([
|
|
4140
|
-
|
|
4324
|
+
Inject13(AUTH_CONFIG),
|
|
4141
4325
|
__metadata18("design:type", Object)
|
|
4142
4326
|
], AuthService.prototype, "config", void 0);
|
|
4143
4327
|
__decorate18([
|
|
@@ -4162,7 +4346,7 @@ __decorate18([
|
|
|
4162
4346
|
], AuthService.prototype, "resetToTemporaryCredential", null);
|
|
4163
4347
|
AuthService = __decorate18([
|
|
4164
4348
|
Injectable12(),
|
|
4165
|
-
__metadata18("design:paramtypes", [typeof (_a12 = typeof TokenService !== "undefined" && TokenService) === "function" ? _a12 : Object, typeof (_b10 = typeof UserService !== "undefined" && UserService) === "function" ? _b10 : Object, typeof (
|
|
4349
|
+
__metadata18("design:paramtypes", [typeof (_a12 = typeof TokenService !== "undefined" && TokenService) === "function" ? _a12 : Object, typeof (_b10 = typeof UserService !== "undefined" && UserService) === "function" ? _b10 : Object, typeof (_c9 = typeof UserValidator !== "undefined" && UserValidator) === "function" ? _c9 : Object, typeof (_d7 = typeof EncryptionService !== "undefined" && EncryptionService) === "function" ? _d7 : Object, typeof (_e5 = typeof CookieManager !== "undefined" && CookieManager) === "function" ? _e5 : Object, typeof (_f4 = typeof I18nService2 !== "undefined" && I18nService2) === "function" ? _f4 : Object, typeof (_g3 = typeof EmailService !== "undefined" && EmailService) === "function" ? _g3 : Object, typeof (_h3 = typeof AuthSessionService !== "undefined" && AuthSessionService) === "function" ? _h3 : Object, typeof (_j2 = typeof CredentialSetupRequirementService !== "undefined" && CredentialSetupRequirementService) === "function" ? _j2 : Object, typeof (_k = typeof PasswordSetupService !== "undefined" && PasswordSetupService) === "function" ? _k : Object])
|
|
4166
4350
|
], AuthService);
|
|
4167
4351
|
|
|
4168
4352
|
// src/auth/AuthGuard.ts
|
|
@@ -4394,7 +4578,7 @@ var loginDto = z.union([
|
|
|
4394
4578
|
})
|
|
4395
4579
|
]);
|
|
4396
4580
|
var changePasswordDto = z.object({
|
|
4397
|
-
currentPassword:
|
|
4581
|
+
currentPassword: loginCredentialField,
|
|
4398
4582
|
newPassword: passwordField
|
|
4399
4583
|
});
|
|
4400
4584
|
var resetPasswordDto = z.object({
|
|
@@ -4617,7 +4801,7 @@ AuthController = __decorate21([
|
|
|
4617
4801
|
], AuthController);
|
|
4618
4802
|
|
|
4619
4803
|
// src/auth/AuthResolver.ts
|
|
4620
|
-
import { APP, Container, DI, Inject as
|
|
4804
|
+
import { APP, Container, DI, Inject as Inject14, LOGGER, Meta, Service as Service4 } from "najm-core";
|
|
4621
4805
|
import { USER, ROLE, PERMISSIONS } from "najm-guard";
|
|
4622
4806
|
var __decorate22 = function(decorators, target, key, desc) {
|
|
4623
4807
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -4755,11 +4939,11 @@ __decorate22([
|
|
|
4755
4939
|
__metadata22("design:type", typeof (_a14 = typeof Container !== "undefined" && Container) === "function" ? _a14 : Object)
|
|
4756
4940
|
], AuthResolver.prototype, "container", void 0);
|
|
4757
4941
|
__decorate22([
|
|
4758
|
-
|
|
4942
|
+
Inject14(APP),
|
|
4759
4943
|
__metadata22("design:type", Object)
|
|
4760
4944
|
], AuthResolver.prototype, "app", void 0);
|
|
4761
4945
|
__decorate22([
|
|
4762
|
-
|
|
4946
|
+
Inject14(LOGGER),
|
|
4763
4947
|
__metadata22("design:type", Object)
|
|
4764
4948
|
], AuthResolver.prototype, "log", void 0);
|
|
4765
4949
|
AuthResolver = __decorate22([
|
|
@@ -4768,7 +4952,7 @@ AuthResolver = __decorate22([
|
|
|
4768
4952
|
], AuthResolver);
|
|
4769
4953
|
|
|
4770
4954
|
// src/auth/AuthIdentityContextService.ts
|
|
4771
|
-
import { DI as DI2, Inject as
|
|
4955
|
+
import { DI as DI2, Inject as Inject15, INJECTION_TYPES, Meta as Meta2, Service as Service5 } from "najm-core";
|
|
4772
4956
|
var __decorate23 = function(decorators, target, key, desc) {
|
|
4773
4957
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4774
4958
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -4812,12 +4996,12 @@ __decorate23([
|
|
|
4812
4996
|
AuthIdentityContextService = __decorate23([
|
|
4813
4997
|
Service5(),
|
|
4814
4998
|
Meta2({ layer: "plugin", order: 14 }),
|
|
4815
|
-
__param6(0,
|
|
4999
|
+
__param6(0, Inject15(AUTH_CONFIG)),
|
|
4816
5000
|
__metadata23("design:paramtypes", [Object])
|
|
4817
5001
|
], AuthIdentityContextService);
|
|
4818
5002
|
|
|
4819
5003
|
// src/auth/RegistrationController.ts
|
|
4820
|
-
import { Body as Body2, Controller as Controller2, Err as Err14, Inject as
|
|
5004
|
+
import { Body as Body2, Controller as Controller2, Err as Err14, Inject as Inject16, Post as Post2, ResMsg as ResMsg2 } from "najm-core";
|
|
4821
5005
|
import { RateLimit as RateLimit2 } from "najm-rate";
|
|
4822
5006
|
import { Public as Public2 } from "najm-guard";
|
|
4823
5007
|
import { Validate as Validate2 } from "najm-validation";
|
|
@@ -4852,7 +5036,7 @@ var RegistrationController = class RegistrationController2 {
|
|
|
4852
5036
|
}
|
|
4853
5037
|
};
|
|
4854
5038
|
__decorate24([
|
|
4855
|
-
|
|
5039
|
+
Inject16(AUTH_CONFIG),
|
|
4856
5040
|
__metadata24("design:type", Object)
|
|
4857
5041
|
], RegistrationController.prototype, "config", void 0);
|
|
4858
5042
|
__decorate24([
|
|
@@ -5320,8 +5504,8 @@ __export(permissions_exports, {
|
|
|
5320
5504
|
});
|
|
5321
5505
|
|
|
5322
5506
|
// src/permissions/PermissionRepository.ts
|
|
5323
|
-
import { eq as eq7, and as
|
|
5324
|
-
import { Repository as Repository6, Inject as
|
|
5507
|
+
import { eq as eq7, and as and5, sql as sql4 } from "drizzle-orm";
|
|
5508
|
+
import { Repository as Repository6, Inject as Inject17 } from "najm-core";
|
|
5325
5509
|
import { DB as DB6 } from "najm-database";
|
|
5326
5510
|
var __decorate27 = function(decorators, target, key, desc) {
|
|
5327
5511
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -5389,16 +5573,58 @@ var PermissionRepository = class PermissionRepository2 {
|
|
|
5389
5573
|
description: this.roles.description
|
|
5390
5574
|
}).from(this.rolePermissions).leftJoin(this.roles, eq7(this.rolePermissions.roleId, this.roles.id)).where(eq7(this.rolePermissions.permissionId, permissionId));
|
|
5391
5575
|
}
|
|
5576
|
+
/**
|
|
5577
|
+
* Capture grants and mutate their parent in one database transaction. A
|
|
5578
|
+
* PostgreSQL parent-row lock conflicts with the FK check of a concurrent
|
|
5579
|
+
* grant. SQLite executes the whole callback synchronously under its write
|
|
5580
|
+
* lock; an async callback would commit before its awaited queries finish.
|
|
5581
|
+
*/
|
|
5582
|
+
async mutateWithRoles(id, mutation) {
|
|
5583
|
+
const grants = /* @__PURE__ */ __name((db) => {
|
|
5584
|
+
const query = db.selectDistinct({ roleId: this.rolePermissions.roleId }).from(this.rolePermissions);
|
|
5585
|
+
return id === void 0 ? query : query.where(eq7(this.rolePermissions.permissionId, id));
|
|
5586
|
+
}, "grants");
|
|
5587
|
+
const probe = this.db.select().from(this.permissions).limit(0);
|
|
5588
|
+
const sqlite = typeof probe.for !== "function";
|
|
5589
|
+
if (sqlite && this.db.resultKind === "sync") {
|
|
5590
|
+
return this.db.transaction((tx) => {
|
|
5591
|
+
const roleIds = grants(tx).all().map((row) => row.roleId);
|
|
5592
|
+
const rows = mutation(tx).returning().all();
|
|
5593
|
+
return { rows, roleIds };
|
|
5594
|
+
});
|
|
5595
|
+
}
|
|
5596
|
+
return this.db.transaction(async (tx) => {
|
|
5597
|
+
if (!sqlite && id === void 0) {
|
|
5598
|
+
await tx.execute(sql4`LOCK TABLE ${this.permissions} IN EXCLUSIVE MODE`);
|
|
5599
|
+
} else if (!sqlite) {
|
|
5600
|
+
await tx.select({ id: this.permissions.id }).from(this.permissions).where(eq7(this.permissions.id, id)).for("update");
|
|
5601
|
+
}
|
|
5602
|
+
const roleIds = (await grants(tx)).map((row) => row.roleId);
|
|
5603
|
+
const rows = await mutation(tx).returning();
|
|
5604
|
+
return { rows, roleIds };
|
|
5605
|
+
});
|
|
5606
|
+
}
|
|
5607
|
+
async updateWithRoles(id, data) {
|
|
5608
|
+
const { rows, roleIds } = await this.mutateWithRoles(id, (db) => db.update(this.permissions).set(data).where(eq7(this.permissions.id, id)));
|
|
5609
|
+
return { permission: rows[0], roleIds };
|
|
5610
|
+
}
|
|
5611
|
+
async deleteWithRoles(id) {
|
|
5612
|
+
const { rows, roleIds } = await this.mutateWithRoles(id, (db) => db.delete(this.permissions).where(eq7(this.permissions.id, id)));
|
|
5613
|
+
return { permission: rows[0], roleIds };
|
|
5614
|
+
}
|
|
5615
|
+
async deleteAllWithRoles() {
|
|
5616
|
+
return this.mutateWithRoles(void 0, (db) => db.delete(this.permissions));
|
|
5617
|
+
}
|
|
5392
5618
|
async assignPermissionToRole(roleId, permissionId) {
|
|
5393
5619
|
const [newRolePermission] = await this.db.insert(this.rolePermissions).values({ roleId, permissionId }).returning();
|
|
5394
5620
|
return newRolePermission;
|
|
5395
5621
|
}
|
|
5396
5622
|
async removePermissionFromRole(roleId, permissionId) {
|
|
5397
|
-
const [deletedRolePermission] = await this.db.delete(this.rolePermissions).where(
|
|
5623
|
+
const [deletedRolePermission] = await this.db.delete(this.rolePermissions).where(and5(eq7(this.rolePermissions.roleId, roleId), eq7(this.rolePermissions.permissionId, permissionId))).returning();
|
|
5398
5624
|
return deletedRolePermission;
|
|
5399
5625
|
}
|
|
5400
5626
|
async checkRoleHasPermission(roleId, permissionId) {
|
|
5401
|
-
const [rolePermission] = await this.db.select().from(this.rolePermissions).where(
|
|
5627
|
+
const [rolePermission] = await this.db.select().from(this.rolePermissions).where(and5(eq7(this.rolePermissions.roleId, roleId), eq7(this.rolePermissions.permissionId, permissionId)));
|
|
5402
5628
|
return !!rolePermission;
|
|
5403
5629
|
}
|
|
5404
5630
|
async deleteAll() {
|
|
@@ -5412,7 +5638,7 @@ __decorate27([
|
|
|
5412
5638
|
__metadata27("design:type", Object)
|
|
5413
5639
|
], PermissionRepository.prototype, "db", void 0);
|
|
5414
5640
|
__decorate27([
|
|
5415
|
-
|
|
5641
|
+
Inject17(AUTH_SCHEMA),
|
|
5416
5642
|
__metadata27("design:type", Object)
|
|
5417
5643
|
], PermissionRepository.prototype, "schema", void 0);
|
|
5418
5644
|
PermissionRepository = __decorate27([
|
|
@@ -5588,8 +5814,8 @@ var __metadata30 = function(k, v) {
|
|
|
5588
5814
|
};
|
|
5589
5815
|
var _a19;
|
|
5590
5816
|
var _b12;
|
|
5591
|
-
var
|
|
5592
|
-
var
|
|
5817
|
+
var _c10;
|
|
5818
|
+
var _d8;
|
|
5593
5819
|
var _e6;
|
|
5594
5820
|
var PermissionService = class PermissionService2 {
|
|
5595
5821
|
static {
|
|
@@ -5615,10 +5841,14 @@ var PermissionService = class PermissionService2 {
|
|
|
5615
5841
|
* sessions that captured it end. This is an infrequent administrative
|
|
5616
5842
|
* action, and the work is proportional to the role's membership.
|
|
5617
5843
|
*/
|
|
5618
|
-
async invalidateRoleHolders(
|
|
5844
|
+
async invalidateRoleHolders(roleIds) {
|
|
5619
5845
|
if (!this.userRepository || !this.sessionInvalidation)
|
|
5620
5846
|
return;
|
|
5621
|
-
const userIds =
|
|
5847
|
+
const userIds = /* @__PURE__ */ new Set();
|
|
5848
|
+
for (const roleId of new Set(typeof roleIds === "string" ? [roleIds] : roleIds)) {
|
|
5849
|
+
for (const userId of await this.userRepository.getIdsByRole(roleId))
|
|
5850
|
+
userIds.add(userId);
|
|
5851
|
+
}
|
|
5622
5852
|
for (const userId of userIds) {
|
|
5623
5853
|
await this.sessionInvalidation.invalidateAccessTokens(userId);
|
|
5624
5854
|
}
|
|
@@ -5643,11 +5873,18 @@ var PermissionService = class PermissionService2 {
|
|
|
5643
5873
|
async update(id, data) {
|
|
5644
5874
|
await this.permissionValidator.checkPermissionExists(id);
|
|
5645
5875
|
await this.permissionValidator.checkPermissionNameUnique(data.name, id);
|
|
5646
|
-
|
|
5876
|
+
const changesClaims = ["name", "resource", "action"].some((field) => data[field] !== void 0);
|
|
5877
|
+
if (!changesClaims)
|
|
5878
|
+
return this.permissionRepository.update(id, data);
|
|
5879
|
+
const { permission: updated, roleIds } = await this.permissionRepository.updateWithRoles(id, data);
|
|
5880
|
+
await this.invalidateRoleHolders(roleIds);
|
|
5881
|
+
return updated;
|
|
5647
5882
|
}
|
|
5648
5883
|
async delete(id) {
|
|
5649
5884
|
await this.permissionValidator.checkPermissionExists(id);
|
|
5650
|
-
|
|
5885
|
+
const { permission: deleted, roleIds } = await this.permissionRepository.deleteWithRoles(id);
|
|
5886
|
+
await this.invalidateRoleHolders(roleIds);
|
|
5887
|
+
return deleted;
|
|
5651
5888
|
}
|
|
5652
5889
|
async getPermissionsByRole(roleId) {
|
|
5653
5890
|
return await this.permissionRepository.getPermissionsByRole(roleId);
|
|
@@ -5716,12 +5953,14 @@ var PermissionService = class PermissionService2 {
|
|
|
5716
5953
|
return { assigned, skipped };
|
|
5717
5954
|
}
|
|
5718
5955
|
async deleteAll() {
|
|
5719
|
-
|
|
5956
|
+
const { rows: deleted, roleIds } = await this.permissionRepository.deleteAllWithRoles();
|
|
5957
|
+
await this.invalidateRoleHolders(roleIds);
|
|
5958
|
+
return deleted;
|
|
5720
5959
|
}
|
|
5721
5960
|
};
|
|
5722
5961
|
PermissionService = __decorate30([
|
|
5723
5962
|
Injectable15(),
|
|
5724
|
-
__metadata30("design:paramtypes", [typeof (_a19 = typeof PermissionRepository !== "undefined" && PermissionRepository) === "function" ? _a19 : Object, typeof (_b12 = typeof PermissionValidator !== "undefined" && PermissionValidator) === "function" ? _b12 : Object, typeof (
|
|
5963
|
+
__metadata30("design:paramtypes", [typeof (_a19 = typeof PermissionRepository !== "undefined" && PermissionRepository) === "function" ? _a19 : Object, typeof (_b12 = typeof PermissionValidator !== "undefined" && PermissionValidator) === "function" ? _b12 : Object, typeof (_c10 = typeof RoleService !== "undefined" && RoleService) === "function" ? _c10 : Object, typeof (_d8 = typeof UserRepository !== "undefined" && UserRepository) === "function" ? _d8 : Object, typeof (_e6 = typeof SessionInvalidationService !== "undefined" && SessionInvalidationService) === "function" ? _e6 : Object])
|
|
5725
5964
|
], PermissionService);
|
|
5726
5965
|
|
|
5727
5966
|
// src/permissions/PermissionController.ts
|
|
@@ -5954,7 +6193,7 @@ var revokeTokenDto = z4.object({
|
|
|
5954
6193
|
});
|
|
5955
6194
|
|
|
5956
6195
|
// src/ownership/scopedOwnership.ts
|
|
5957
|
-
import { aliasedTable, eq as eq8, getTableColumns, sql as
|
|
6196
|
+
import { aliasedTable, eq as eq8, getTableColumns, sql as sql5 } from "drizzle-orm";
|
|
5958
6197
|
var DEFAULT_ADMIN_ROLES = ["admin"];
|
|
5959
6198
|
var DRIZZLE_NAME = Symbol.for("drizzle:Name");
|
|
5960
6199
|
var DRIZZLE_BASE_NAME = Symbol.for("drizzle:BaseName");
|
|
@@ -6091,7 +6330,7 @@ var OwnershipToken = class {
|
|
|
6091
6330
|
return query;
|
|
6092
6331
|
const rule = this._rules[role];
|
|
6093
6332
|
if (!rule)
|
|
6094
|
-
return query.where(
|
|
6333
|
+
return query.where(sql5`1 = 0`);
|
|
6095
6334
|
const { query: q, condition } = rule(uid, query);
|
|
6096
6335
|
return q.where(condition);
|
|
6097
6336
|
}
|
|
@@ -6106,7 +6345,7 @@ var OwnershipToken = class {
|
|
|
6106
6345
|
return { query, condition: null };
|
|
6107
6346
|
const rule = this._rules[role];
|
|
6108
6347
|
if (!rule)
|
|
6109
|
-
return { query, condition:
|
|
6348
|
+
return { query, condition: sql5`1 = 0` };
|
|
6110
6349
|
return rule(uid, query);
|
|
6111
6350
|
}
|
|
6112
6351
|
};
|
|
@@ -6116,7 +6355,7 @@ function own(table, opts) {
|
|
|
6116
6355
|
__name(own, "own");
|
|
6117
6356
|
|
|
6118
6357
|
// src/ownership/configureOwnership.ts
|
|
6119
|
-
import { Injectable as Injectable16, Inject as
|
|
6358
|
+
import { Injectable as Injectable16, Inject as Inject18, User as User4, Body as Body6, Params as Params4 } from "najm-core";
|
|
6120
6359
|
import { createGuard as createGuard5, composeGuards as composeGuards4 } from "najm-guard";
|
|
6121
6360
|
var __decorate32 = function(decorators, target, key, desc) {
|
|
6122
6361
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -6157,7 +6396,7 @@ function createResourceGuards(ownershipClass, resourceType, resource, options) {
|
|
|
6157
6396
|
}
|
|
6158
6397
|
};
|
|
6159
6398
|
__decorate32([
|
|
6160
|
-
|
|
6399
|
+
Inject18(ownershipClass),
|
|
6161
6400
|
__metadata32("design:type", Object)
|
|
6162
6401
|
], AccessGuard.prototype, "ownership", void 0);
|
|
6163
6402
|
__decorate32([
|
|
@@ -6181,7 +6420,7 @@ function createResourceGuards(ownershipClass, resourceType, resource, options) {
|
|
|
6181
6420
|
}
|
|
6182
6421
|
};
|
|
6183
6422
|
__decorate32([
|
|
6184
|
-
|
|
6423
|
+
Inject18(ownershipClass),
|
|
6185
6424
|
__metadata32("design:type", Object)
|
|
6186
6425
|
], ListGuard.prototype, "ownership", void 0);
|
|
6187
6426
|
__decorate32([
|
|
@@ -6340,7 +6579,7 @@ function configureOwnership(config) {
|
|
|
6340
6579
|
}
|
|
6341
6580
|
};
|
|
6342
6581
|
__decorate32([
|
|
6343
|
-
|
|
6582
|
+
Inject18(GeneratedOwnershipService),
|
|
6344
6583
|
__metadata32("design:type", GeneratedOwnershipService)
|
|
6345
6584
|
], BodyAccessGuard.prototype, "ownership", void 0);
|
|
6346
6585
|
__decorate32([
|
|
@@ -6465,12 +6704,12 @@ __name(Policy, "Policy");
|
|
|
6465
6704
|
|
|
6466
6705
|
// src/ownership/OwnedDecorator.ts
|
|
6467
6706
|
import "reflect-metadata";
|
|
6468
|
-
import { sql as
|
|
6469
|
-
import { Injectable as Injectable17, Inject as
|
|
6707
|
+
import { sql as sql7, and as and6 } from "drizzle-orm";
|
|
6708
|
+
import { Injectable as Injectable17, Inject as Inject19, DI as DI3, Container as Container2, REQUEST_ID } from "najm-core";
|
|
6470
6709
|
import { USER as USER2 } from "najm-guard";
|
|
6471
6710
|
|
|
6472
6711
|
// src/ownership/ownershipCondition.ts
|
|
6473
|
-
import { inArray, or, sql as
|
|
6712
|
+
import { inArray, or, sql as sql6 } from "drizzle-orm";
|
|
6474
6713
|
function validateOwnershipAlternatives(tokens) {
|
|
6475
6714
|
if (!tokens.length)
|
|
6476
6715
|
throw new Error("Ownership requires at least one token");
|
|
@@ -6489,7 +6728,7 @@ function ownershipCondition(db, tokens, context) {
|
|
|
6489
6728
|
return void 0;
|
|
6490
6729
|
const user = context.getUser();
|
|
6491
6730
|
if (!user)
|
|
6492
|
-
return
|
|
6731
|
+
return sql6`1 = 0`;
|
|
6493
6732
|
const conditions = [];
|
|
6494
6733
|
for (const token of tokens) {
|
|
6495
6734
|
const { query, condition } = token.applyScopeSplit(user.id, user.role, db.select({ id: idColumn }).from(token.table));
|
|
@@ -6500,7 +6739,7 @@ function ownershipCondition(db, tokens, context) {
|
|
|
6500
6739
|
conditions.push(inArray(idColumn, query.where(condition)));
|
|
6501
6740
|
}
|
|
6502
6741
|
if (!conditions.length)
|
|
6503
|
-
return
|
|
6742
|
+
return sql6`1 = 0`;
|
|
6504
6743
|
return conditions.length === 1 ? conditions[0] : or(...conditions);
|
|
6505
6744
|
}
|
|
6506
6745
|
__name(ownershipCondition, "ownershipCondition");
|
|
@@ -6531,7 +6770,7 @@ function ownedProperty(proto, key, tokens) {
|
|
|
6531
6770
|
enumerable: false
|
|
6532
6771
|
});
|
|
6533
6772
|
if (!Reflect.hasOwnMetadata(OWNED_PROPERTIES, proto, "_scopeCtx")) {
|
|
6534
|
-
|
|
6773
|
+
Inject19(ScopeContext)(proto, "_scopeCtx");
|
|
6535
6774
|
Reflect.defineMetadata(OWNED_PROPERTIES, true, proto, "_scopeCtx");
|
|
6536
6775
|
Object.defineProperty(proto, "_scopeCtx", {
|
|
6537
6776
|
get() {
|
|
@@ -6605,7 +6844,7 @@ function Owned(token, ...alternatives) {
|
|
|
6605
6844
|
throw new Error("@Owned requires a repository class");
|
|
6606
6845
|
Reflect.defineMetadata(OWNED_META, token, target);
|
|
6607
6846
|
const proto = target.prototype;
|
|
6608
|
-
|
|
6847
|
+
Inject19(ScopeContext)(proto, "_scopeCtx");
|
|
6609
6848
|
Object.defineProperty(proto, "ownershipCondition", {
|
|
6610
6849
|
value() {
|
|
6611
6850
|
return ownershipCondition(this.db, tokens, this._scopeCtx);
|
|
@@ -6627,7 +6866,7 @@ function Owned(token, ...alternatives) {
|
|
|
6627
6866
|
return null;
|
|
6628
6867
|
if (filtered.length === 1)
|
|
6629
6868
|
return filtered[0];
|
|
6630
|
-
return
|
|
6869
|
+
return and6(...filtered);
|
|
6631
6870
|
}
|
|
6632
6871
|
__name(combineConditions, "combineConditions");
|
|
6633
6872
|
function applyQueryOptions(query, opts = {}, whereCondition) {
|
|
@@ -6648,7 +6887,7 @@ function Owned(token, ...alternatives) {
|
|
|
6648
6887
|
return query.where(this.ownershipCondition());
|
|
6649
6888
|
const user = getUser(this);
|
|
6650
6889
|
if (!user)
|
|
6651
|
-
return query.where(
|
|
6890
|
+
return query.where(sql7`1 = 0`);
|
|
6652
6891
|
return token.applyScope(user.id, user.role, query);
|
|
6653
6892
|
};
|
|
6654
6893
|
},
|
|
@@ -6827,7 +7066,7 @@ __name(getAuthLocale, "getAuthLocale");
|
|
|
6827
7066
|
var AUTH_SUPPORTED_LANGUAGES = Object.keys(AUTH_LOCALES);
|
|
6828
7067
|
|
|
6829
7068
|
// src/oauth/google/GoogleOAuthProvider.ts
|
|
6830
|
-
import { Inject as
|
|
7069
|
+
import { Inject as Inject21, Injectable as Injectable19 } from "najm-core";
|
|
6831
7070
|
|
|
6832
7071
|
// src/oauth/types.ts
|
|
6833
7072
|
var OAuthFlowError = class extends Error {
|
|
@@ -6845,7 +7084,7 @@ var OAuthFlowError = class extends Error {
|
|
|
6845
7084
|
};
|
|
6846
7085
|
|
|
6847
7086
|
// src/oauth/google/GoogleTokenVerifier.ts
|
|
6848
|
-
import { Inject as
|
|
7087
|
+
import { Inject as Inject20, Injectable as Injectable18 } from "najm-core";
|
|
6849
7088
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
6850
7089
|
var __decorate34 = function(decorators, target, key, desc) {
|
|
6851
7090
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -6906,7 +7145,7 @@ var GoogleTokenVerifier = class GoogleTokenVerifier2 {
|
|
|
6906
7145
|
}
|
|
6907
7146
|
};
|
|
6908
7147
|
__decorate34([
|
|
6909
|
-
|
|
7148
|
+
Inject20(AUTH_CONFIG),
|
|
6910
7149
|
__metadata34("design:type", Object)
|
|
6911
7150
|
], GoogleTokenVerifier.prototype, "config", void 0);
|
|
6912
7151
|
GoogleTokenVerifier = __decorate34([
|
|
@@ -6992,7 +7231,7 @@ var GoogleOAuthProvider = class GoogleOAuthProvider2 {
|
|
|
6992
7231
|
}
|
|
6993
7232
|
};
|
|
6994
7233
|
__decorate35([
|
|
6995
|
-
|
|
7234
|
+
Inject21(AUTH_CONFIG),
|
|
6996
7235
|
__metadata35("design:type", Object)
|
|
6997
7236
|
], GoogleOAuthProvider.prototype, "config", void 0);
|
|
6998
7237
|
GoogleOAuthProvider = __decorate35([
|
|
@@ -7001,7 +7240,7 @@ GoogleOAuthProvider = __decorate35([
|
|
|
7001
7240
|
], GoogleOAuthProvider);
|
|
7002
7241
|
|
|
7003
7242
|
// src/oauth/github/GitHubOAuthProvider.ts
|
|
7004
|
-
import { Inject as
|
|
7243
|
+
import { Inject as Inject22, Injectable as Injectable20 } from "najm-core";
|
|
7005
7244
|
var __decorate36 = function(decorators, target, key, desc) {
|
|
7006
7245
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
7007
7246
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -7125,7 +7364,7 @@ var GitHubOAuthProvider = class GitHubOAuthProvider2 {
|
|
|
7125
7364
|
}
|
|
7126
7365
|
};
|
|
7127
7366
|
__decorate36([
|
|
7128
|
-
|
|
7367
|
+
Inject22(AUTH_CONFIG),
|
|
7129
7368
|
__metadata36("design:type", Object)
|
|
7130
7369
|
], GitHubOAuthProvider.prototype, "config", void 0);
|
|
7131
7370
|
GitHubOAuthProvider = __decorate36([
|
|
@@ -7139,16 +7378,16 @@ import { RateLimit as RateLimit3 } from "najm-rate";
|
|
|
7139
7378
|
import { Public as Public3 } from "najm-guard";
|
|
7140
7379
|
|
|
7141
7380
|
// src/oauth/OAuthService.ts
|
|
7142
|
-
import { Err as Err16, Inject as
|
|
7381
|
+
import { Err as Err16, Inject as Inject25, Injectable as Injectable23, Log as Log2 } from "najm-core";
|
|
7143
7382
|
|
|
7144
7383
|
// src/oauth/OAuthAccountService.ts
|
|
7145
7384
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
7146
|
-
import { Inject as
|
|
7385
|
+
import { Inject as Inject24, Injectable as Injectable21 } from "najm-core";
|
|
7147
7386
|
import { Transaction as Transaction5 } from "najm-database";
|
|
7148
7387
|
|
|
7149
7388
|
// src/oauth/OAuthAccountRepository.ts
|
|
7150
|
-
import { and as
|
|
7151
|
-
import { Inject as
|
|
7389
|
+
import { and as and7, eq as eq9 } from "drizzle-orm";
|
|
7390
|
+
import { Inject as Inject23, Repository as Repository7 } from "najm-core";
|
|
7152
7391
|
import { DB as DB7 } from "najm-database";
|
|
7153
7392
|
var __decorate37 = function(decorators, target, key, desc) {
|
|
7154
7393
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -7172,11 +7411,11 @@ var OAuthAccountRepository = class OAuthAccountRepository2 {
|
|
|
7172
7411
|
return this.schema.oauthAccounts;
|
|
7173
7412
|
}
|
|
7174
7413
|
async getByProviderAccount(provider, providerAccountId) {
|
|
7175
|
-
const [account] = await this.db.select().from(this.accounts).where(
|
|
7414
|
+
const [account] = await this.db.select().from(this.accounts).where(and7(eq9(this.accounts.provider, provider), eq9(this.accounts.providerAccountId, providerAccountId))).limit(1);
|
|
7176
7415
|
return account;
|
|
7177
7416
|
}
|
|
7178
7417
|
async getByUserProvider(userId, provider) {
|
|
7179
|
-
const [account] = await this.db.select().from(this.accounts).where(
|
|
7418
|
+
const [account] = await this.db.select().from(this.accounts).where(and7(eq9(this.accounts.userId, userId), eq9(this.accounts.provider, provider))).limit(1);
|
|
7180
7419
|
return account;
|
|
7181
7420
|
}
|
|
7182
7421
|
async create(data) {
|
|
@@ -7189,7 +7428,7 @@ __decorate37([
|
|
|
7189
7428
|
__metadata37("design:type", Object)
|
|
7190
7429
|
], OAuthAccountRepository.prototype, "db", void 0);
|
|
7191
7430
|
__decorate37([
|
|
7192
|
-
|
|
7431
|
+
Inject23(AUTH_SCHEMA),
|
|
7193
7432
|
__metadata37("design:type", Object)
|
|
7194
7433
|
], OAuthAccountRepository.prototype, "schema", void 0);
|
|
7195
7434
|
OAuthAccountRepository = __decorate37([
|
|
@@ -7208,8 +7447,8 @@ var __metadata38 = function(k, v) {
|
|
|
7208
7447
|
};
|
|
7209
7448
|
var _a23;
|
|
7210
7449
|
var _b13;
|
|
7211
|
-
var
|
|
7212
|
-
var
|
|
7450
|
+
var _c11;
|
|
7451
|
+
var _d9;
|
|
7213
7452
|
var OAuthAccountService = class OAuthAccountService2 {
|
|
7214
7453
|
static {
|
|
7215
7454
|
__name(this, "OAuthAccountService");
|
|
@@ -7286,20 +7525,20 @@ var OAuthAccountService = class OAuthAccountService2 {
|
|
|
7286
7525
|
}
|
|
7287
7526
|
};
|
|
7288
7527
|
__decorate38([
|
|
7289
|
-
|
|
7528
|
+
Inject24(AUTH_CONFIG),
|
|
7290
7529
|
__metadata38("design:type", Object)
|
|
7291
7530
|
], OAuthAccountService.prototype, "config", void 0);
|
|
7292
7531
|
__decorate38([
|
|
7293
7532
|
Transaction5(),
|
|
7294
7533
|
__metadata38("design:type", Function),
|
|
7295
7534
|
__metadata38("design:paramtypes", [Object]),
|
|
7296
|
-
__metadata38("design:returntype", typeof (
|
|
7535
|
+
__metadata38("design:returntype", typeof (_c11 = typeof Promise !== "undefined" && Promise) === "function" ? _c11 : Object)
|
|
7297
7536
|
], OAuthAccountService.prototype, "resolveForLogin", null);
|
|
7298
7537
|
__decorate38([
|
|
7299
7538
|
Transaction5(),
|
|
7300
7539
|
__metadata38("design:type", Function),
|
|
7301
7540
|
__metadata38("design:paramtypes", [String, Object]),
|
|
7302
|
-
__metadata38("design:returntype", typeof (
|
|
7541
|
+
__metadata38("design:returntype", typeof (_d9 = typeof Promise !== "undefined" && Promise) === "function" ? _d9 : Object)
|
|
7303
7542
|
], OAuthAccountService.prototype, "linkUser", null);
|
|
7304
7543
|
OAuthAccountService = __decorate38([
|
|
7305
7544
|
Injectable21(),
|
|
@@ -7310,6 +7549,22 @@ OAuthAccountService = __decorate38([
|
|
|
7310
7549
|
import { createHash as createHash4, randomBytes as randomBytes4, timingSafeEqual } from "crypto";
|
|
7311
7550
|
import { Injectable as Injectable22 } from "najm-core";
|
|
7312
7551
|
import { CookieService as CookieService3 } from "najm-cookies";
|
|
7552
|
+
|
|
7553
|
+
// src/client/redirectPath.ts
|
|
7554
|
+
function normalizeLocalRedirectPath(value) {
|
|
7555
|
+
if (typeof value !== "string" || !value.startsWith("/") || value.startsWith("//") || value.includes("\\") || /[\u0000-\u001f\u007f]/.test(value)) return null;
|
|
7556
|
+
try {
|
|
7557
|
+
const base = new URL("https://najm.invalid");
|
|
7558
|
+
const parsed = new URL(value, base);
|
|
7559
|
+
if (parsed.origin !== base.origin || parsed.pathname.startsWith("//")) return null;
|
|
7560
|
+
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
7561
|
+
} catch {
|
|
7562
|
+
return null;
|
|
7563
|
+
}
|
|
7564
|
+
}
|
|
7565
|
+
__name(normalizeLocalRedirectPath, "normalizeLocalRedirectPath");
|
|
7566
|
+
|
|
7567
|
+
// src/oauth/OAuthStateService.ts
|
|
7313
7568
|
var __decorate39 = function(decorators, target, key, desc) {
|
|
7314
7569
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
7315
7570
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -7385,22 +7640,10 @@ var OAuthStateService = class OAuthStateService2 {
|
|
|
7385
7640
|
}
|
|
7386
7641
|
}
|
|
7387
7642
|
validateReturnTo(value) {
|
|
7388
|
-
const
|
|
7389
|
-
if (!
|
|
7390
|
-
throw new OAuthFlowError("oauth_redirect_invalid");
|
|
7391
|
-
}
|
|
7392
|
-
try {
|
|
7393
|
-
const base = new URL("https://najm.invalid");
|
|
7394
|
-
const parsed = new URL(candidate, base);
|
|
7395
|
-
if (parsed.origin !== base.origin || parsed.username || parsed.password) {
|
|
7396
|
-
throw new OAuthFlowError("oauth_redirect_invalid");
|
|
7397
|
-
}
|
|
7398
|
-
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
7399
|
-
} catch (error) {
|
|
7400
|
-
if (error instanceof OAuthFlowError)
|
|
7401
|
-
throw error;
|
|
7643
|
+
const path2 = normalizeLocalRedirectPath(value?.trim() || "/");
|
|
7644
|
+
if (!path2)
|
|
7402
7645
|
throw new OAuthFlowError("oauth_redirect_invalid");
|
|
7403
|
-
|
|
7646
|
+
return path2;
|
|
7404
7647
|
}
|
|
7405
7648
|
cookieName(provider, state) {
|
|
7406
7649
|
return `${COOKIE_PREFIX}${provider}.${state}`;
|
|
@@ -7426,12 +7669,12 @@ var __metadata40 = function(k, v) {
|
|
|
7426
7669
|
};
|
|
7427
7670
|
var _a25;
|
|
7428
7671
|
var _b15;
|
|
7429
|
-
var
|
|
7430
|
-
var
|
|
7672
|
+
var _c12;
|
|
7673
|
+
var _d10;
|
|
7431
7674
|
var _e7;
|
|
7432
7675
|
var _f5;
|
|
7433
7676
|
var _g4;
|
|
7434
|
-
var
|
|
7677
|
+
var _h4;
|
|
7435
7678
|
var OAuthService = class OAuthService2 {
|
|
7436
7679
|
static {
|
|
7437
7680
|
__name(this, "OAuthService");
|
|
@@ -7597,7 +7840,7 @@ var OAuthService = class OAuthService2 {
|
|
|
7597
7840
|
}
|
|
7598
7841
|
};
|
|
7599
7842
|
__decorate40([
|
|
7600
|
-
|
|
7843
|
+
Inject25(AUTH_CONFIG),
|
|
7601
7844
|
__metadata40("design:type", Object)
|
|
7602
7845
|
], OAuthService.prototype, "config", void 0);
|
|
7603
7846
|
__decorate40([
|
|
@@ -7606,7 +7849,7 @@ __decorate40([
|
|
|
7606
7849
|
], OAuthService.prototype, "logger", void 0);
|
|
7607
7850
|
OAuthService = __decorate40([
|
|
7608
7851
|
Injectable23(),
|
|
7609
|
-
__metadata40("design:paramtypes", [typeof (_a25 = typeof OAuthStateService !== "undefined" && OAuthStateService) === "function" ? _a25 : Object, typeof (_b15 = typeof GoogleOAuthProvider !== "undefined" && GoogleOAuthProvider) === "function" ? _b15 : Object, typeof (
|
|
7852
|
+
__metadata40("design:paramtypes", [typeof (_a25 = typeof OAuthStateService !== "undefined" && OAuthStateService) === "function" ? _a25 : Object, typeof (_b15 = typeof GoogleOAuthProvider !== "undefined" && GoogleOAuthProvider) === "function" ? _b15 : Object, typeof (_c12 = typeof GitHubOAuthProvider !== "undefined" && GitHubOAuthProvider) === "function" ? _c12 : Object, typeof (_d10 = typeof OAuthAccountService !== "undefined" && OAuthAccountService) === "function" ? _d10 : Object, typeof (_e7 = typeof AuthSessionService !== "undefined" && AuthSessionService) === "function" ? _e7 : Object, typeof (_f5 = typeof TokenService !== "undefined" && TokenService) === "function" ? _f5 : Object, typeof (_g4 = typeof UserService !== "undefined" && UserService) === "function" ? _g4 : Object, typeof (_h4 = typeof CredentialSetupRequirementService !== "undefined" && CredentialSetupRequirementService) === "function" ? _h4 : Object])
|
|
7610
7853
|
], OAuthService);
|
|
7611
7854
|
|
|
7612
7855
|
// src/oauth/GitHubOAuthController.ts
|