@nymphjs/tilmeld 1.0.0-alpha.9 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/User.ts CHANGED
@@ -7,12 +7,11 @@ import {
7
7
  } from '@nymphjs/nymph';
8
8
  import { EmailOptions } from 'email-templates';
9
9
  import strtotime from 'locutus/php/datetime/strtotime';
10
- import { nanoid, customAlphabet } from 'nanoid';
11
- import { nolookalikesSafe } from 'nanoid-dictionary';
12
10
  import Base64 from 'crypto-js/enc-base64';
13
11
  import sha256 from 'crypto-js/sha256';
14
12
  import md5 from 'crypto-js/md5';
15
13
  import { difference } from 'lodash';
14
+ import { humanSecret, nanoid } from '@nymphjs/guid';
16
15
 
17
16
  import type Tilmeld from './Tilmeld';
18
17
  import AbleObject from './AbleObject';
@@ -24,6 +23,44 @@ import {
24
23
  EmailChangeRateLimitExceededError,
25
24
  } from './errors';
26
25
 
26
+ export type EventType =
27
+ | 'beforeRegister'
28
+ | 'afterRegister'
29
+ | 'beforeLogin'
30
+ | 'afterLogin'
31
+ | 'beforeLogout'
32
+ | 'afterLogout';
33
+ /**
34
+ * Theses are run before the user data checks, so the only checks before are
35
+ * whether registration is allowed and whether the user is already registered.
36
+ */
37
+ export type TilmeldBeforeRegisterCallback = (
38
+ user: User & UserData,
39
+ data: { password: string; additionalData?: { [k: string]: any } }
40
+ ) => Promise<void> | void;
41
+ export type TilmeldAfterRegisterCallback = (
42
+ user: User & UserData,
43
+ result: { loggedin: boolean; message: string }
44
+ ) => Promise<void> | void;
45
+ /**
46
+ * These are run after the authentication checks, but before the login action.
47
+ */
48
+ export type TilmeldBeforeLoginCallback = (
49
+ user: User & UserData,
50
+ data: {
51
+ username: string;
52
+ password: string;
53
+ additionalData?: { [k: string]: any };
54
+ }
55
+ ) => void;
56
+ /**
57
+ * This is run before the transaction is committed, and you can perform
58
+ * additional functions on the transaction, which is available in `user.$nymph`.
59
+ */
60
+ export type TilmeldAfterLoginCallback = (user: User & UserData) => void;
61
+ export type TilmeldBeforeLogoutCallback = (user: User & UserData) => void;
62
+ export type TilmeldAfterLogoutCallback = (user: User & UserData) => void;
63
+
27
64
  export type UserData = {
28
65
  /**
29
66
  * The abilities granted to the user.
@@ -142,6 +179,12 @@ export default class User extends AbleObject<UserData> {
142
179
  static tilmeld: Tilmeld;
143
180
  static ETYPE = 'tilmeld_user';
144
181
  static class = 'User';
182
+ private static beforeRegisterCallbacks: TilmeldBeforeRegisterCallback[] = [];
183
+ private static afterRegisterCallbacks: TilmeldAfterRegisterCallback[] = [];
184
+ private static beforeLoginCallbacks: TilmeldBeforeLoginCallback[] = [];
185
+ private static afterLoginCallbacks: TilmeldAfterLoginCallback[] = [];
186
+ private static beforeLogoutCallbacks: TilmeldBeforeLogoutCallback[] = [];
187
+ private static afterLogoutCallbacks: TilmeldAfterLogoutCallback[] = [];
145
188
 
146
189
  private static DEFAULT_CLIENT_ENABLED_METHODS = [
147
190
  '$checkUsername',
@@ -325,7 +368,7 @@ export default class User extends AbleObject<UserData> {
325
368
  }
326
369
 
327
370
  // Create a unique secret.
328
- getUser.recoverSecret = customAlphabet(nolookalikesSafe, 10)();
371
+ getUser.recoverSecret = humanSecret();
329
372
  getUser.recoverSecretDate = Date.now();
330
373
  if (!(await getUser.$saveSkipAC())) {
331
374
  return { result: false, message: "Couldn't save recovery secret." };
@@ -429,7 +472,11 @@ export default class User extends AbleObject<UserData> {
429
472
  };
430
473
  }
431
474
 
432
- public static async loginUser(data: { username: string; password: string }) {
475
+ public static async loginUser(data: {
476
+ username: string;
477
+ password: string;
478
+ additionalData?: { [k: string]: any };
479
+ }) {
433
480
  if (!('username' in data) || !data.username.length) {
434
481
  return { result: false, message: 'Incorrect login/password.' };
435
482
  }
@@ -443,7 +490,11 @@ export default class User extends AbleObject<UserData> {
443
490
  return result;
444
491
  }
445
492
 
446
- public $login(data: { username: string; password: string }) {
493
+ public $login(data: {
494
+ username: string;
495
+ password: string;
496
+ additionalData?: { [k: string]: any };
497
+ }) {
447
498
  const tilmeld = this.$nymph.tilmeld as Tilmeld;
448
499
  if (this.guid == null) {
449
500
  return { result: false, message: 'Incorrect login/password.' };
@@ -458,11 +509,32 @@ export default class User extends AbleObject<UserData> {
458
509
  return { result: false, message: 'Incorrect login/password.' };
459
510
  }
460
511
 
512
+ try {
513
+ for (let callback of (this.constructor as typeof User)
514
+ .beforeLoginCallbacks) {
515
+ if (callback) {
516
+ callback(this, data);
517
+ }
518
+ }
519
+ } catch (e: any) {
520
+ return {
521
+ result: false,
522
+ message: e.message,
523
+ };
524
+ }
525
+
461
526
  // Authentication was successful, attempt to login.
462
527
  if (!tilmeld.login(this, true)) {
463
528
  return { result: false, message: 'Incorrect login/password.' };
464
529
  }
465
530
 
531
+ for (let callback of (this.constructor as typeof User)
532
+ .afterLoginCallbacks) {
533
+ if (callback) {
534
+ callback(this);
535
+ }
536
+ }
537
+
466
538
  // Login was successful.
467
539
  return { result: true, message: 'You are logged in.' };
468
540
  }
@@ -473,7 +545,30 @@ export default class User extends AbleObject<UserData> {
473
545
  */
474
546
  public $logout() {
475
547
  const tilmeld = this.$nymph.tilmeld as Tilmeld;
548
+
549
+ try {
550
+ for (let callback of (this.constructor as typeof User)
551
+ .beforeLogoutCallbacks) {
552
+ if (callback) {
553
+ callback(this);
554
+ }
555
+ }
556
+ } catch (e: any) {
557
+ return {
558
+ result: false,
559
+ message: e.message,
560
+ };
561
+ }
562
+
476
563
  tilmeld.logout();
564
+
565
+ for (let callback of (this.constructor as typeof User)
566
+ .afterLogoutCallbacks) {
567
+ if (callback) {
568
+ callback(this);
569
+ }
570
+ }
571
+
477
572
  return { result: true, message: 'You have been logged out.' };
478
573
  }
479
574
 
@@ -1123,8 +1218,9 @@ export default class User extends AbleObject<UserData> {
1123
1218
 
1124
1219
  public async $register(data: {
1125
1220
  password: string;
1221
+ additionalData?: { [k: string]: any };
1126
1222
  }): Promise<{ result: boolean; loggedin: boolean; message: string }> {
1127
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1223
+ let tilmeld = this.$nymph.tilmeld as Tilmeld;
1128
1224
  if (!tilmeld.config.allowRegistration) {
1129
1225
  return {
1130
1226
  result: false,
@@ -1139,6 +1235,22 @@ export default class User extends AbleObject<UserData> {
1139
1235
  message: 'This is already a registered user.',
1140
1236
  };
1141
1237
  }
1238
+
1239
+ try {
1240
+ for (let callback of (this.constructor as typeof User)
1241
+ .beforeRegisterCallbacks) {
1242
+ if (callback) {
1243
+ await callback(this, data);
1244
+ }
1245
+ }
1246
+ } catch (e: any) {
1247
+ return {
1248
+ result: false,
1249
+ loggedin: false,
1250
+ message: e.message,
1251
+ };
1252
+ }
1253
+
1142
1254
  if (!('password' in data) || !data.password.length) {
1143
1255
  return {
1144
1256
  result: false,
@@ -1170,6 +1282,7 @@ export default class User extends AbleObject<UserData> {
1170
1282
  const nymph = this.$nymph;
1171
1283
  const tnymph = await this.$nymph.startTransaction(transaction);
1172
1284
  this.$nymph = tnymph;
1285
+ tilmeld = this.$nymph.tilmeld as Tilmeld;
1173
1286
 
1174
1287
  try {
1175
1288
  // Add primary group.
@@ -1318,19 +1431,40 @@ export default class User extends AbleObject<UserData> {
1318
1431
  tilmeld.config.unverifiedAccess &&
1319
1432
  !madeAdmin
1320
1433
  ) {
1321
- tilmeld.login(this, true);
1434
+ if (!tilmeld.login(this, true)) {
1435
+ throw new Error('An error occurred trying to log you in.');
1436
+ }
1322
1437
  message +=
1323
1438
  "You're now logged in! An email has been sent to " +
1324
1439
  `${this.$data.email} with a verification link for you to finish ` +
1325
1440
  'registration.';
1326
1441
  loggedin = true;
1327
1442
  } else {
1328
- tilmeld.login(this, true);
1443
+ if (!tilmeld.login(this, true)) {
1444
+ throw new Error('An error occurred trying to log you in.');
1445
+ }
1329
1446
  message += "You're now registered and logged in!";
1330
1447
  loggedin = true;
1331
1448
  }
1332
- await tnymph.commit(transaction);
1333
- this.$nymph = nymph;
1449
+
1450
+ for (let callback of (this.constructor as typeof User)
1451
+ .afterRegisterCallbacks) {
1452
+ if (callback) {
1453
+ await callback(this, {
1454
+ loggedin,
1455
+ message,
1456
+ });
1457
+ }
1458
+ }
1459
+
1460
+ try {
1461
+ await tnymph.commit(transaction);
1462
+ this.$nymph = nymph;
1463
+ this.$nymph.tilmeld?.fillSession(this);
1464
+ } catch (e: any) {
1465
+ throw e;
1466
+ }
1467
+
1334
1468
  return {
1335
1469
  result: true,
1336
1470
  loggedin,
@@ -1353,7 +1487,7 @@ export default class User extends AbleObject<UserData> {
1353
1487
  }
1354
1488
 
1355
1489
  public async $save() {
1356
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1490
+ let tilmeld = this.$nymph.tilmeld as Tilmeld;
1357
1491
  if (this.$data.username == null || !this.$data.username.trim().length) {
1358
1492
  return false;
1359
1493
  }
@@ -1551,6 +1685,7 @@ export default class User extends AbleObject<UserData> {
1551
1685
  const nymph = this.$nymph;
1552
1686
  const tnymph = await this.$nymph.startTransaction(transaction);
1553
1687
  this.$nymph = tnymph;
1688
+ tilmeld = this.$nymph.tilmeld as Tilmeld;
1554
1689
 
1555
1690
  if (
1556
1691
  this.$data.group != null &&
@@ -1597,14 +1732,14 @@ export default class User extends AbleObject<UserData> {
1597
1732
  }
1598
1733
  }
1599
1734
 
1600
- if (tilmeld.User.current(true).$is(this)) {
1601
- // Update the user in the session cache.
1602
- tilmeld.fillSession(this);
1603
- }
1604
-
1605
1735
  this.$descendantGroups = undefined;
1606
1736
  this.$gatekeeperCache = undefined;
1607
1737
  await tnymph.commit(transaction);
1738
+
1739
+ if ((nymph.tilmeld as Tilmeld).User.current(true).$is(this)) {
1740
+ // Update the user in the session cache.
1741
+ (nymph.tilmeld as Tilmeld).fillSession(this);
1742
+ }
1608
1743
  } else {
1609
1744
  await tnymph.rollback(transaction);
1610
1745
  }
@@ -1646,4 +1781,82 @@ export default class User extends AbleObject<UserData> {
1646
1781
  }
1647
1782
  return await super.$delete();
1648
1783
  }
1784
+
1785
+ public static on<T extends EventType>(
1786
+ event: T,
1787
+ callback: T extends 'beforeRegister'
1788
+ ? TilmeldBeforeRegisterCallback
1789
+ : T extends 'afterRegister'
1790
+ ? TilmeldAfterRegisterCallback
1791
+ : T extends 'beforeLogin'
1792
+ ? TilmeldBeforeLoginCallback
1793
+ : T extends 'afterLogin'
1794
+ ? TilmeldAfterLoginCallback
1795
+ : T extends 'beforeLogout'
1796
+ ? TilmeldBeforeLogoutCallback
1797
+ : T extends 'afterLogout'
1798
+ ? TilmeldAfterLogoutCallback
1799
+ : never
1800
+ ) {
1801
+ const prop = (event + 'Callbacks') as T extends 'beforeRegister'
1802
+ ? 'beforeRegisterCallbacks'
1803
+ : T extends 'afterRegister'
1804
+ ? 'afterRegisterCallbacks'
1805
+ : T extends 'beforeLogin'
1806
+ ? 'beforeLoginCallbacks'
1807
+ : T extends 'afterLogin'
1808
+ ? 'afterLoginCallbacks'
1809
+ : T extends 'beforeLogout'
1810
+ ? 'beforeLogoutCallbacks'
1811
+ : T extends 'afterLogout'
1812
+ ? 'afterLogoutCallbacks'
1813
+ : never;
1814
+ if (!(prop in this)) {
1815
+ throw new Error('Invalid event type.');
1816
+ }
1817
+ // @ts-ignore: The callback should always be the right type here.
1818
+ this[prop].push(callback);
1819
+ return () => this.off(event, callback);
1820
+ }
1821
+
1822
+ public static off<T extends EventType>(
1823
+ event: T,
1824
+ callback: T extends 'beforeRegister'
1825
+ ? TilmeldBeforeRegisterCallback
1826
+ : T extends 'afterRegister'
1827
+ ? TilmeldAfterRegisterCallback
1828
+ : T extends 'beforeLogin'
1829
+ ? TilmeldBeforeLoginCallback
1830
+ : T extends 'afterLogin'
1831
+ ? TilmeldAfterLoginCallback
1832
+ : T extends 'beforeLogout'
1833
+ ? TilmeldBeforeLogoutCallback
1834
+ : T extends 'afterLogout'
1835
+ ? TilmeldAfterLogoutCallback
1836
+ : never
1837
+ ) {
1838
+ const prop = (event + 'Callbacks') as T extends 'beforeRegister'
1839
+ ? 'beforeRegisterCallbacks'
1840
+ : T extends 'afterRegister'
1841
+ ? 'afterRegisterCallbacks'
1842
+ : T extends 'beforeLogin'
1843
+ ? 'beforeLoginCallbacks'
1844
+ : T extends 'afterLogin'
1845
+ ? 'afterLoginCallbacks'
1846
+ : T extends 'beforeLogout'
1847
+ ? 'beforeLogoutCallbacks'
1848
+ : T extends 'afterLogout'
1849
+ ? 'afterLogoutCallbacks'
1850
+ : never;
1851
+ if (!(prop in this)) {
1852
+ return false;
1853
+ }
1854
+ // @ts-ignore: The callback should always be the right type here.
1855
+ const i = this[prop].indexOf(callback);
1856
+ if (i > -1) {
1857
+ // @ts-ignore: The callback should always be the right type here.
1858
+ this[prop].splice(i, 1);
1859
+ }
1860
+ return true;
1861
+ }
1649
1862
  }
package/src/conf/d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { EmailOptions } from 'email-templates';
1
+ import { EmailOptions, EmailConfig } from 'email-templates';
2
2
 
3
3
  import type Tilmeld from '../Tilmeld';
4
4
  import Group, { GroupData } from '../Group';
@@ -245,6 +245,18 @@ export interface Config {
245
245
  * email sender.
246
246
  */
247
247
  emailTemplateDir: string;
248
+ /**
249
+ * Configure an email object to prepare for sending.
250
+ *
251
+ * You can use this, for example, for DKIM signing.
252
+ *
253
+ * See: https://github.com/forwardemail/email-templates/blob/v8.0.9/README.md#Options
254
+ */
255
+ configEmail: (
256
+ tilmeld: Tilmeld,
257
+ options: EmailOptions,
258
+ user: User & UserData
259
+ ) => Promise<EmailConfig>;
248
260
  /**
249
261
  * Send an email to a user. Uses `email-templates` by default.
250
262
  *
@@ -1,9 +1,10 @@
1
- import { nymphJoiProps } from '@nymphjs/nymph';
2
1
  import path from 'path';
2
+ import { URL } from 'url';
3
3
  import jwt from 'jsonwebtoken';
4
- import { nanoid } from 'nanoid';
5
4
  import Email from 'email-templates';
6
5
  import Joi from 'joi';
6
+ import { nanoid } from '@nymphjs/guid';
7
+ import { nymphJoiProps } from '@nymphjs/nymph';
7
8
 
8
9
  import { Config } from './d';
9
10
  import User from '../User';
@@ -107,16 +108,21 @@ export default {
107
108
  }
108
109
  },
109
110
  emailTemplateDir: path.join(__dirname, '..', '..', 'emails'),
110
- sendEmail: async (tilmeld, options, user) => {
111
+ configEmail: async (tilmeld, _options, _user) => {
111
112
  const appUrl = new URL(tilmeld.config.appUrl);
112
- const email = new Email({
113
+ return {
113
114
  message: {
114
115
  from: {
115
116
  name: tilmeld.config.appName,
116
117
  address: `noreply@${appUrl.hostname}`,
117
118
  },
118
119
  },
119
- });
120
+ };
121
+ },
122
+ sendEmail: async (tilmeld, options, user) => {
123
+ const email = new Email(
124
+ await tilmeld.config.configEmail(tilmeld, options, user)
125
+ );
120
126
  try {
121
127
  const result = await email.send({
122
128
  ...options,
@@ -159,7 +165,7 @@ export default {
159
165
  group.$getValidatable(),
160
166
  Joi.object().keys({
161
167
  ...nymphJoiProps,
162
- groupname: Joi.string().trim(false).min(1).required(),
168
+ groupname: Joi.string().trim(false).required(),
163
169
  enabled: Joi.boolean().required(),
164
170
  email: Joi.string()
165
171
  .trim(false)
@@ -171,7 +177,6 @@ export default {
171
177
  name: 'control characters',
172
178
  invert: true,
173
179
  })
174
- .min(1)
175
180
  .max(512)
176
181
  .required(),
177
182
  avatar: Joi.string()
@@ -181,7 +186,6 @@ export default {
181
186
  name: 'control characters',
182
187
  invert: true,
183
188
  })
184
- .min(1)
185
189
  .max(1024),
186
190
  phone: Joi.string()
187
191
  .trim(false)
@@ -194,7 +198,6 @@ export default {
194
198
  name: 'control characters',
195
199
  invert: true,
196
200
  })
197
- .min(1)
198
201
  .max(256)
199
202
  ),
200
203
  defaultPrimary: Joi.boolean(),
@@ -209,7 +212,7 @@ export default {
209
212
  user.$getValidatable(),
210
213
  Joi.object().keys({
211
214
  ...nymphJoiProps,
212
- username: Joi.string().trim(false).min(1).required(),
215
+ username: Joi.string().trim(false).required(),
213
216
  enabled: Joi.boolean().required(),
214
217
  email: Joi.string()
215
218
  .trim(false)
@@ -221,7 +224,6 @@ export default {
221
224
  name: 'control characters',
222
225
  invert: true,
223
226
  })
224
- .min(1)
225
227
  .max(512)
226
228
  .required(),
227
229
  nameMiddle: Joi.string()
@@ -230,7 +232,6 @@ export default {
230
232
  name: 'control characters',
231
233
  invert: true,
232
234
  })
233
- .min(1)
234
235
  .max(512),
235
236
  nameLast: Joi.string()
236
237
  .trim(false)
@@ -238,7 +239,6 @@ export default {
238
239
  name: 'control characters',
239
240
  invert: true,
240
241
  })
241
- .min(1)
242
242
  .max(512),
243
243
  name: Joi.string()
244
244
  .trim(false)
@@ -246,7 +246,6 @@ export default {
246
246
  name: 'control characters',
247
247
  invert: true,
248
248
  })
249
- .min(1)
250
249
  .max(512)
251
250
  .required(),
252
251
  avatar: Joi.string()
@@ -256,7 +255,6 @@ export default {
256
255
  name: 'control characters',
257
256
  invert: true,
258
257
  })
259
- .min(1)
260
258
  .max(1024),
261
259
  phone: Joi.string()
262
260
  .trim(false)
@@ -269,7 +267,6 @@ export default {
269
267
  name: 'control characters',
270
268
  invert: true,
271
269
  })
272
- .min(1)
273
270
  .max(256)
274
271
  ),
275
272
  inheritAbilities: Joi.boolean(),
@@ -309,8 +306,8 @@ export default {
309
306
  })
310
307
  .length(10),
311
308
  recoverSecretDate: Joi.number(),
312
- salt: Joi.string().trim(false).min(1),
313
- password: Joi.string().trim(false).min(1).required(),
309
+ salt: Joi.string().trim(false),
310
+ password: Joi.string().trim(false).required(),
314
311
  }),
315
312
  'Invalid User: '
316
313
  );
package/tsconfig.json CHANGED
@@ -2,6 +2,8 @@
2
2
  "extends": "@tsconfig/recommended/tsconfig.json",
3
3
 
4
4
  "compilerOptions": {
5
+ "lib": ["ES2021"],
6
+ "target": "ES2021",
5
7
  "noImplicitAny": true,
6
8
  "removeComments": true,
7
9
  "sourceMap": true,