@nu-art/user-account-backend 0.401.8 → 0.401.9

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.
@@ -2,7 +2,7 @@ import { DB_BaseObject, Dispatcher } from '@nu-art/ts-common';
2
2
  import { firestore } from 'firebase-admin';
3
3
  import { ModuleBE_BaseDB } from '@nu-art/thunderstorm-backend';
4
4
  import { FirestoreQuery } from '@nu-art/firebase-shared';
5
- import { _SessionKey_Account, Account_ChangePassword, Account_ChangeThumbnail, Account_CreateAccount, Account_Login, Account_RegisterAccount, Account_SetPassword, AccountEmail, AccountEmailWithDevice, AccountToAssertPassword, AccountToSpice, AccountType, DB_Account, DBProto_Account, PasswordAssertionConfig, SafeDB_Account, UI_Account } from '@nu-art/user-account-shared';
5
+ import { _SessionKey_Account, Account_ChangePassword, Account_ChangeThumbnail, Account_CreateAccount, Account_Delete, Account_Login, Account_RegisterAccount, Account_SetPassword, AccountEmail, AccountEmailWithDevice, AccountToAssertPassword, AccountToSpice, AccountType, DB_Account, DBProto_Account, PasswordAssertionConfig, SafeDB_Account, UI_Account } from '@nu-art/user-account-shared';
6
6
  import { BaseSessionClaims, CollectSessionData } from '../session/index.js';
7
7
  import Transaction = firestore.Transaction;
8
8
  type BaseAccount = {
@@ -25,6 +25,9 @@ export interface OnPreLogout {
25
25
  }
26
26
  export declare const dispatch_onAccountLogin: Dispatcher<OnUserLogin, "__onUserLogin", [account: SafeDB_Account, transaction: firestore.Transaction], void>;
27
27
  export declare const dispatch_onPreLogout: Dispatcher<OnPreLogout, "__onPreLogout", [], void>;
28
+ export interface OnAccountDeleted {
29
+ __onAccountDeleted: (account: SafeDB_Account, transaction: Transaction) => Promise<void>;
30
+ }
28
31
  type Config = {
29
32
  canRegister: boolean;
30
33
  passwordAssertion?: PasswordAssertionConfig;
@@ -35,7 +38,6 @@ export declare class ModuleBE_AccountDB_Class extends ModuleBE_BaseDB<DBProto_Ac
35
38
  constructor();
36
39
  init(): void;
37
40
  manipulateQuery(query: FirestoreQuery<DB_Account>): FirestoreQuery<DB_Account>;
38
- canDeleteItems(dbItems: DB_Account[], transaction?: FirebaseFirestore.Transaction): Promise<void>;
39
41
  __collectSessionData(data: BaseSessionClaims): Promise<{
40
42
  key: "account";
41
43
  value: {
@@ -84,6 +86,7 @@ export declare class ModuleBE_AccountDB_Class extends ModuleBE_BaseDB<DBProto_Ac
84
86
  sessions: import("@nu-art/user-account-shared").DB_Session[];
85
87
  }>;
86
88
  changeThumbnail: (request: Account_ChangeThumbnail["request"]) => Promise<Account_ChangeThumbnail["response"]>;
89
+ delete: (request: Account_Delete["request"]) => Promise<Account_Delete["response"]>;
87
90
  };
88
91
  password: {
89
92
  assertPasswordExistence: (email: string, password?: string, passwordCheck?: string) => void;
@@ -8,6 +8,7 @@ import { ModuleBE_FailedLoginAttemptDB } from '../failed-login-attempt/index.js'
8
8
  export const dispatch_onAccountLogin = new Dispatcher('__onUserLogin');
9
9
  const dispatch_onAccountRegistered = new Dispatcher('__onNewUserRegistered');
10
10
  export const dispatch_onPreLogout = new Dispatcher('__onPreLogout');
11
+ const dispatch_OnAccountDeleted = new Dispatcher('__onAccountDeleted');
11
12
  export class ModuleBE_AccountDB_Class extends ModuleBE_BaseDB {
12
13
  Middleware = async () => {
13
14
  const account = SessionKey_Account_BE.get();
@@ -33,11 +34,12 @@ export class ModuleBE_AccountDB_Class extends ModuleBE_BaseDB {
33
34
  createBodyServerApi(ApiDef_Account._v1.setPassword, this.account.setPassword),
34
35
  createQueryServerApi(ApiDef_Account._v1.getSessions, this.account.getSessions),
35
36
  createBodyServerApi(ApiDef_Account._v1.changeThumbnail, this.account.changeThumbnail),
37
+ createQueryServerApi(ApiDef_Account._v1.deleteAccount, this.account.delete),
36
38
  createQueryServerApi(ApiDef_Account._v1.getPasswordAssertionConfig, async () => ({
37
39
  config: this.config.ignorePasswordAssertion
38
40
  ? undefined
39
41
  : this.config.passwordAssertion
40
- }))
42
+ })),
41
43
  ]);
42
44
  }
43
45
  manipulateQuery(query) {
@@ -46,9 +48,9 @@ export class ModuleBE_AccountDB_Class extends ModuleBE_BaseDB {
46
48
  select: ['__created', '_v', '__updated', 'email', '_newPasswordRequired', 'type', '_id', 'thumbnail', 'displayName', '_auditorId', 'description']
47
49
  };
48
50
  }
49
- canDeleteItems(dbItems, transaction) {
50
- throw HttpCodes._5XX.NOT_IMPLEMENTED('Account Deletion is not implemented yet');
51
- }
51
+ // canDeleteItems(dbItems: DB_Account[], transaction?: FirebaseFirestore.Transaction): Promise<void> {
52
+ // throw HttpCodes._5XX.NOT_IMPLEMENTED('Account Deletion is not implemented yet');
53
+ // }
52
54
  async __collectSessionData(data) {
53
55
  const account = await this.query.uniqueAssert(data.accountId);
54
56
  return {
@@ -279,6 +281,26 @@ export class ModuleBE_AccountDB_Class extends ModuleBE_BaseDB {
279
281
  return {
280
282
  account: (await account.get()),
281
283
  };
284
+ },
285
+ delete: async (request) => {
286
+ return await this.runTransaction(async (t) => {
287
+ const account = await this.query.unique(request.accountId);
288
+ if (!account)
289
+ throw HttpCodes._4XX.NOT_FOUND(`Account with id ${request.accountId} Not Found!`);
290
+ try {
291
+ const safeAccount = makeAccountSafe(account);
292
+ await dispatch_OnAccountDeleted.dispatchModuleAsyncSerial(safeAccount, t);
293
+ await this.delete.item(account, t);
294
+ return { account };
295
+ }
296
+ catch (err) {
297
+ const error = err;
298
+ if (error.responseCode === 422)
299
+ throw error;
300
+ this.logError('Failed deleting account', err);
301
+ throw HttpCodes._5XX.INTERNAL_SERVER_ERROR('Failed to delete account', error.message, error);
302
+ }
303
+ });
282
304
  }
283
305
  };
284
306
  password = {
@@ -1,7 +1,8 @@
1
1
  import { AnyPrimitive, Dispatcher, RecursiveObjectOfPrimitives, TypedKeyValue, UniqueId } from '@nu-art/ts-common';
2
2
  import { firestore } from 'firebase-admin';
3
3
  import { DBApiConfigV3, ModuleBE_BaseDB } from '@nu-art/thunderstorm-backend';
4
- import { DB_Session, DBProto_Session } from '@nu-art/user-account-shared';
4
+ import { DB_Session, DBProto_Session, SafeDB_Account } from '@nu-art/user-account-shared';
5
+ import { OnAccountDeleted } from '../account/ModuleBE_AccountDB.js';
5
6
  import Transaction = firestore.Transaction;
6
7
  export type BaseSessionClaims = {
7
8
  accountId: string;
@@ -29,8 +30,9 @@ type Config = DBApiConfigV3<DBProto_Session> & {
29
30
  };
30
31
  export declare class ModuleBE_SessionDB_Class extends ModuleBE_BaseDB<DBProto_Session, Config> implements CollectSessionData<TypedKeyValue<'session', {
31
32
  deviceId: string;
32
- }>> {
33
+ }>>, OnAccountDeleted {
33
34
  private jwtHandler;
35
+ __onAccountDeleted: (account: SafeDB_Account, transaction: Transaction) => Promise<void>;
34
36
  constructor();
35
37
  init(): void;
36
38
  private collectSessionData;
@@ -1,16 +1,20 @@
1
- import { ApiException, currentTimeMillis, Day, Dispatcher, filterInstances, filterKeys, isErrorOfType, JwtTools, md5, MUSTNeverHappenException } from '@nu-art/ts-common';
1
+ import { ApiException, currentTimeMillis, Day, dbObjectToId, Dispatcher, filterKeys, isErrorOfType, JwtTools, md5, MUSTNeverHappenException } from '@nu-art/ts-common';
2
2
  import { ModuleBE_BaseDB } from '@nu-art/thunderstorm-backend';
3
- import { DBDef_Session } from '@nu-art/user-account-shared';
3
+ import { AccountType_Service, DBDef_Session } from '@nu-art/user-account-shared';
4
4
  import { Header_Authorization, MemKey_DB_Session, MemKey_Jwt, MemKey_SessionData, SessionKey_Account_BE } from './consts.js';
5
5
  import { MemKey_HttpResponse } from '@nu-art/thunderstorm-backend/modules/server/consts';
6
6
  import { ResponseHeaderKey_JWTToken } from '@nu-art/thunderstorm-shared';
7
7
  import { ModuleBE_JWT } from './ModuleBE_JWT.js';
8
8
  import { HttpCodes } from '@nu-art/ts-common/core/exceptions/http-codes';
9
9
  import { _EmptyQuery } from '@nu-art/firebase-shared';
10
+ import { ModuleBE_AccountDB } from '../account/ModuleBE_AccountDB.js';
10
11
  export const dispatch_CollectSessionData = new Dispatcher('__collectSessionData');
11
12
  export const Const_Default_SessionJWT_SecretKey = 'jwt-signer--account-session';
12
13
  export class ModuleBE_SessionDB_Class extends ModuleBE_BaseDB {
13
14
  jwtHandler;
15
+ __onAccountDeleted = async (account, transaction) => {
16
+ await this.delete.where({ accountId: account._id }, transaction);
17
+ };
14
18
  constructor() {
15
19
  super(DBDef_Session);
16
20
  this.setDefaultConfig({
@@ -144,6 +148,9 @@ export class ModuleBE_SessionDB_Class extends ModuleBE_BaseDB {
144
148
  label: content.initialClaims.label,
145
149
  sessionIdJwt: jwt,
146
150
  }, ['linkedSessionId', 'label']);
151
+ const idsToDelete = dbSession.validSessionJwtMd5s.slice(1);
152
+ if (idsToDelete.length)
153
+ await this.delete.all(idsToDelete, transaction);
147
154
  return await this.set.item(dbSession, transaction);
148
155
  },
149
156
  create: Object.assign(async (content, ttlInMs, transaction) => {
@@ -210,7 +217,15 @@ export class ModuleBE_SessionDB_Class extends ModuleBE_BaseDB {
210
217
  async locateSession(jwt) {
211
218
  try {
212
219
  const { dbSession, claims } = await this.runTransaction(async (t) => {
213
- let dbSession = await this._session.query.byJwt(jwt);
220
+ let dbSession;
221
+ try {
222
+ dbSession = await this._session.query.byJwt(jwt);
223
+ }
224
+ catch (err) {
225
+ if (isErrorOfType(err, ApiException)?.responseCode === HttpCodes._4XX.NOT_FOUND.code)
226
+ throw HttpCodes._4XX.UNAUTHORIZED('JWT received in request was not found', err);
227
+ throw err;
228
+ }
214
229
  const latestJwtValidationResult = await this.jwtHandler.verifySignature(dbSession.sessionIdJwt);
215
230
  if (!latestJwtValidationResult.validated)
216
231
  throw new MUSTNeverHappenException(`JWT received from DB is invalid Session id = ${dbSession._id}`);
@@ -239,15 +254,46 @@ export class ModuleBE_SessionDB_Class extends ModuleBE_BaseDB {
239
254
  }
240
255
  cleanOldOrExpiredSessions = async () => {
241
256
  const sessions = await this.query.custom(_EmptyQuery);
242
- const toDelete = filterInstances(await Promise.all(sessions.map(async (session) => {
257
+ const accounts = await ModuleBE_AccountDB.query.where({ type: { $neq: AccountType_Service } });
258
+ const validAccountIds = new Set(accounts.map(dbObjectToId));
259
+ const sessionIdsToDelete = new Set();
260
+ const accountSessionMap = {};
261
+ this.logWarning(`#### Cleaning ${sessions.length} sessions for ${validAccountIds.size} accounts ####`);
262
+ //First pass - Collect all sessions that are referenced by newer sessions
263
+ sessions.forEach(session => {
264
+ if (validAccountIds.has(session.accountId)) {
265
+ const currentSession = accountSessionMap[session.accountId];
266
+ if (!currentSession || currentSession.__created < session.__created) {
267
+ accountSessionMap[session.accountId] = session;
268
+ if (currentSession)
269
+ sessionIdsToDelete.add(currentSession._id);
270
+ }
271
+ else {
272
+ sessionIdsToDelete.add(session._id);
273
+ }
274
+ }
275
+ if (!session.validSessionJwtMd5s?.length)
276
+ sessionIdsToDelete.add(session._id);
277
+ else
278
+ session.validSessionJwtMd5s.forEach(id => {
279
+ if (id !== session._id)
280
+ sessionIdsToDelete.add(id);
281
+ });
282
+ });
283
+ //Second pass - collect all sessions that are expired or has the old "sessionData" property in their decoded data
284
+ await Promise.all(sessions.map(async (session) => {
285
+ if (sessionIdsToDelete.has(session._id))
286
+ return;
243
287
  const isExpired = await JwtTools.isJwtExpired(session.sessionIdJwt);
244
288
  if (isExpired)
245
- return session;
289
+ return sessionIdsToDelete.add(session._id);
246
290
  const decoded = await JwtTools.decode(session.sessionIdJwt);
247
291
  if ('sessionData' in decoded)
248
- return session;
249
- })));
250
- await this.delete.allItems(toDelete);
292
+ return sessionIdsToDelete.add(session._id);
293
+ }));
294
+ //Delete sessions
295
+ await this.delete.all(Array.from(sessionIdsToDelete));
296
+ this.logWarning(`### Deleted ${sessionIdsToDelete.size} Sessions! ###`);
251
297
  };
252
298
  }
253
299
  export const ModuleBE_SessionDB = new ModuleBE_SessionDB_Class();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nu-art/user-account-backend",
3
- "version": "0.401.8",
3
+ "version": "0.401.9",
4
4
  "description": "User Account Backend",
5
5
  "keywords": [
6
6
  "TacB0sS",
@@ -34,14 +34,14 @@
34
34
  "test": "ts-mocha -w -p src/test/tsconfig.json --timeout 0 --inspect=8107 --watch-files 'src/test/**/*.test.ts' src/test/**/*.test.ts"
35
35
  },
36
36
  "dependencies": {
37
- "@nu-art/user-account-shared": "0.401.8",
38
- "@nu-art/firebase-backend": "0.401.8",
39
- "@nu-art/firebase-shared": "0.401.8",
40
- "@nu-art/slack-backend": "0.401.8",
41
- "@nu-art/slack-shared": "0.401.8",
42
- "@nu-art/thunderstorm-backend": "0.401.8",
43
- "@nu-art/thunderstorm-shared": "0.401.8",
44
- "@nu-art/ts-common": "0.401.8",
37
+ "@nu-art/user-account-shared": "0.401.9",
38
+ "@nu-art/firebase-backend": "0.401.9",
39
+ "@nu-art/firebase-shared": "0.401.9",
40
+ "@nu-art/slack-backend": "0.401.9",
41
+ "@nu-art/slack-shared": "0.401.9",
42
+ "@nu-art/thunderstorm-backend": "0.401.9",
43
+ "@nu-art/thunderstorm-shared": "0.401.9",
44
+ "@nu-art/ts-common": "0.401.9",
45
45
  "express": "^4.18.2",
46
46
  "firebase": "^11.9.0",
47
47
  "firebase-admin": "13.4.0",
@@ -53,12 +53,11 @@
53
53
  "xmlbuilder": "^15.1.1"
54
54
  },
55
55
  "devDependencies": {
56
- "@nu-art/testalot": "0.401.8",
57
- "@nu-art/google-services-backend": "0.401.8",
56
+ "@nu-art/testalot": "0.401.9",
57
+ "@nu-art/google-services-backend": "0.401.9",
58
58
  "@types/react": "^18.0.0",
59
59
  "@types/express": "^4.17.17",
60
60
  "@types/history": "^4.7.2",
61
- "@types/request": "^2.48.1",
62
61
  "@types/saml2-js": "^1.6.8",
63
62
  "@types/pako": "^2.0.0",
64
63
  "@types/jsonwebtoken": "^9.0.6"