@xeplr/auth 1.0.1 → 1.0.2

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/bin/migrate.js CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  const path = require('path');
7
7
  const { up, status } = require('@xeplr/db').sqlMigrator;
8
- const { resolveConfig } = require('@xeplr/db');
8
+ const { resolveConfig, migrationsFor } = require('@xeplr/db');
9
9
 
10
10
  /**
11
11
  * xeplr-auth-migrate
@@ -15,14 +15,24 @@ const { resolveConfig } = require('@xeplr/db');
15
15
  * base data lives in migrations so it runs exactly once). Reuses xeplr-db's
16
16
  * sqlMigrator — hand-written .sql files, no down(), ledger-tracked.
17
17
  *
18
- * The app points at its extension dir ONCE via env: AUTH_EXT_MIGRATIONS_DIR
19
- * (or --extDir). Base migrations run first, then the app's — one ledger.
18
+ * The app points at its extension dir(s) via env: XEPLR_AUTH_MIGRATIONS
19
+ * (or --extDir). Base migrations run first, then the extension dir(s) — one
20
+ * shared ledger, tracked by filename.
21
+ *
22
+ * XEPLR_AUTH_MIGRATIONS accepts a COMMA-SEPARATED list, not just one path —
23
+ * this is how several consuming apps' extension migrations (workflow's,
24
+ * jobs', BI's own) can all land in the SAME target database from a single
25
+ * controlled run, without any one app's migrations folder having to contain
26
+ * another's:
27
+ * XEPLR_AUTH_MIGRATIONS=/path/to/bi/migrations-auth,/path/to/workflow/migrations-auth
28
+ * Sibling apps must keep their migration FILENAMES distinct from each
29
+ * other's — the ledger is keyed by filename alone, not by which directory it
30
+ * came from.
20
31
  *
21
32
  * Usage:
22
- * xeplr-auth-migrate up [--extDir <dir>] [--db <database>]
23
- * xeplr-auth-migrate status [--extDir <dir>] [--db <database>]
33
+ * xeplr-auth-migrate up [--extDir <dir>[,<dir>...]] [--db <database>]
34
+ * xeplr-auth-migrate status [--extDir <dir>[,<dir>...]] [--db <database>]
24
35
  */
25
-
26
36
  function parseArgs(argv) {
27
37
  const args = { _: [] };
28
38
  for (let i = 0; i < argv.length; i++) {
@@ -42,13 +52,17 @@ async function main() {
42
52
  const command = args._[0];
43
53
 
44
54
  // Bundled auth migrations (base) + the app's extension migrations dir, which
45
- // the app configures ONCE via AUTH_EXT_MIGRATIONS_DIR (or --extDir). Base runs
55
+ // the app configures ONCE via XEPLR_AUTH_MIGRATIONS (or --extDir). Base runs
46
56
  // first, then the extension — one shared ledger.
47
57
  const options = {
48
58
  ...args,
49
59
  db: args.db || process.env.AUTH_DB_NAME,
50
60
  dir: path.join(__dirname, '..', 'migrations'),
51
- extDir: args.extDir || args['ext-dir'] || process.env.AUTH_EXT_MIGRATIONS_DIR,
61
+ // Same convention as every other entry point and every other library —
62
+ // splitting and existence-checking live in @xeplr/db's
63
+ // resolveDirectories(), not here. This CLI having had its own parseExtDir
64
+ // while boot() had none is exactly how the two diverged.
65
+ extDir: args.extDir || args['ext-dir'] || migrationsFor('auth'),
52
66
  connectionName: args['connection-name'] || args.connectionName || 'auth'
53
67
  };
54
68
 
@@ -92,10 +106,10 @@ async function main() {
92
106
  console.log('xeplr-auth-migrate - Auth database migrations');
93
107
  console.log('');
94
108
  console.log('Commands:');
95
- console.log(' up [--extDir <dir>] Run base + app extension migrations');
96
- console.log(' status [--extDir <dir>] Show migration status');
109
+ console.log(' up [--extDir <dir>[,<dir>...]] Run base + app extension migrations');
110
+ console.log(' status [--extDir <dir>[,<dir>...]] Show migration status');
97
111
  console.log('');
98
- console.log(' App extension dir: --extDir or AUTH_EXT_MIGRATIONS_DIR env');
112
+ console.log(' App extension dir(s): --extDir or XEPLR_AUTH_MIGRATIONS env (comma-separated for more than one)');
99
113
  console.log('');
100
114
  console.log('Options:');
101
115
  console.log(' --db Database name (or AUTH_DB_NAME env)');
package/bin/server.js CHANGED
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * env: ENCRYPTION_KEY · AUTH_DB_CONNECTION_INFO_ENCRYPTED · AUTH_DB_NAME ·
9
9
  * AUTH_JWT_SECRET · AUTH_PORT · AUTH_ACTIVATION_BASE_URL ·
10
- * AUTH_ACCESS_TOKEN_TTL_MINUTES · AUTH_EXT_MIGRATIONS_DIR · email vars · REDIS_*
10
+ * AUTH_ACCESS_TOKEN_TTL_MINUTES · XEPLR_AUTH_MIGRATIONS · email vars · REDIS_*
11
11
  */
12
12
  require('../index').boot().then(function () {
13
13
  if (process.send) process.send({ status: 'ready' });
package/index.js CHANGED
@@ -1,5 +1,12 @@
1
1
  const path = require('path');
2
- const { getConnection, bindModels, resolveConfig, sqlMigrator } = require('@xeplr/db');
2
+ const { getConnection, bindModels, resolveConfig, sqlMigrator, resolveDbConnection, describeDbConnection, migrationsFor, migrationsVar } = require('@xeplr/db');
3
+
4
+ // The connection comes from XEPLR_DB_CONNECTION unless this service overrides
5
+ // it with AUTH_DB_CONNECTION_INFO_ENCRYPTED — one server, one credential, with
6
+ // an escape hatch. AUTH_DB_NAME is untouched: which DATABASE is a separate
7
+ // question from where the server is, and auth keeps its own.
8
+ var AUTH_CONN_VAR = 'AUTH_DB_CONNECTION_INFO_ENCRYPTED';
9
+ function authConnection() { return resolveDbConnection(AUTH_CONN_VAR); }
3
10
  const { createApp } = require('@xeplr/base-apis');
4
11
  const authHelper = require('./lib/authHelper');
5
12
  const { seedSuperAdmin } = require('./lib/seed');
@@ -107,19 +114,25 @@ async function start(config = {}) {
107
114
  // system, so it reads its whole setup from AUTH_* env (see requiredEnv + README):
108
115
  // connection AUTH_DB_CONNECTION_INFO_ENCRYPTED · db AUTH_DB_NAME · AUTH_JWT_SECRET
109
116
  // AUTH_PORT · AUTH_ACTIVATION_BASE_URL · AUTH_ACCESS_TOKEN_TTL_MINUTES
110
- // AUTH_EXT_MIGRATIONS_DIR · email vars · REDIS_*
117
+ // XEPLR_AUTH_MIGRATIONS · email vars · REDIS_*
111
118
  async function boot() {
112
119
  var connName = 'auth';
113
120
  var database = process.env.AUTH_DB_NAME || 'auth';
114
121
 
115
- await resolveConfig(connName, process.env.AUTH_DB_CONNECTION_INFO_ENCRYPTED);
122
+ await resolveConfig(connName, authConnection());
116
123
 
117
124
  // Self-migrate: auth's own .sql migrations (schema + base data), then the
118
125
  // app's extension dir. Idempotent — same as `xeplr-auth-migrate up`.
119
126
  var migrationResult = await sqlMigrator.up({
120
127
  db: database,
121
128
  dir: path.join(__dirname, 'migrations'),
122
- extDir: process.env.AUTH_EXT_MIGRATIONS_DIR || null,
129
+ // The SHARED convention, not this library's own reading of the variable.
130
+ // Passing process.env.XEPLR_AUTH_MIGRATIONS straight through is what
131
+ // broke here: a comma-separated list became one nonexistent directory and
132
+ // this entry point silently applied none of BI's, workflow's or jobs'
133
+ // migrations, while the xeplr-auth-migrate CLI — which had its own
134
+ // splitting — applied all of them. Same variable, two behaviours.
135
+ extDir: migrationsFor('auth'),
123
136
  type: 'precede',
124
137
  connectionName: connName
125
138
  });
@@ -129,12 +142,18 @@ async function boot() {
129
142
 
130
143
  // Configure email (via @xeplr/utils, the engine auth uses) + activation links.
131
144
  require('@xeplr/utils').configureFromEnv();
132
- if (process.env.AUTH_ACTIVATION_BASE_URL) authService.configureActivation(process.env.AUTH_ACTIVATION_BASE_URL);
145
+ authService.configureActivation({
146
+ // Fully qualified, one per link — the token is the only thing appended.
147
+ // AUTH_ACTIVATION_BASE_URL still works as an origin; see configureActivation.
148
+ activationUrl: process.env.AUTH_ACTIVATION_URL || null,
149
+ inviteUrl: process.env.AUTH_INVITE_URL || null,
150
+ baseUrl: process.env.AUTH_ACTIVATION_BASE_URL || null
151
+ });
133
152
 
134
153
  var server = await start({
135
154
  database: database,
136
155
  connectionName: connName,
137
- connection: process.env.AUTH_DB_CONNECTION_INFO_ENCRYPTED,
156
+ connection: authConnection(),
138
157
  port: process.env.AUTH_PORT,
139
158
  jwt: {
140
159
  secret: process.env.AUTH_JWT_SECRET,
@@ -153,15 +172,19 @@ function banner() {
153
172
  var rows = [
154
173
  ['port', process.env.AUTH_PORT || '19001'],
155
174
  ['database', process.env.AUTH_DB_NAME || 'auth'],
156
- ['ext migrations', process.env.AUTH_EXT_MIGRATIONS_DIR || '(none)'],
157
- ['activation url', process.env.AUTH_ACTIVATION_BASE_URL || '(none)'],
175
+ [migrationsVar('auth'), migrationsFor('auth') || '(none)'],
176
+ ['activation url', process.env.AUTH_ACTIVATION_URL || process.env.AUTH_ACTIVATION_BASE_URL || '(none)'],
177
+ ['invite url', process.env.AUTH_INVITE_URL || '(derived)'],
158
178
  ['access token ttl', (process.env.AUTH_ACCESS_TOKEN_TTL_MINUTES || '15') + 'm'],
159
179
  ['refresh token ttl', (process.env.AUTH_REFRESH_TOKEN_TTL_DAYS || '7') + 'd'],
160
180
  ['slide tolerance', (process.env.AUTH_ACCESS_TOKEN_TOLERANCE_SECONDS || '0') + 's'],
161
181
  ['max sessions/user', process.env.AUTH_MAX_SESSIONS_PER_USER || '5'],
162
182
  ['redis', (process.env.REDIS_HOST || 'localhost') + ':' + (process.env.REDIS_PORT || '6379')],
163
183
  ['email', process.env.EMAIL_PROVIDER || '(not configured)'],
164
- ['connection', set(process.env.AUTH_DB_CONNECTION_INFO_ENCRYPTED)],
184
+ // Names the VARIABLE it resolved through, not just "set" — with a shared
185
+ // default and a per-service override, "which server am I on" is otherwise
186
+ // a guess.
187
+ ['connection', describeDbConnection(AUTH_CONN_VAR) || '✗ NOT CONFIGURED'],
165
188
  ['encryption key', set(process.env.ENCRYPTION_KEY)],
166
189
  ['jwt secret', process.env.AUTH_JWT_SECRET ? '✓ set' : '✗ INSECURE DEFAULT']
167
190
  ];
@@ -195,10 +218,16 @@ function attach() {
195
218
  var _ready = (async function () {
196
219
  _conn = await getConnection(
197
220
  process.env.AUTH_DB_NAME || 'auth',
198
- process.env.AUTH_DB_CONNECTION_INFO_ENCRYPTED,
221
+ authConnection(),
199
222
  { bind: false, connectionName: 'auth' }
200
223
  );
201
- if (process.env.AUTH_ACTIVATION_BASE_URL) authService.configureActivation(process.env.AUTH_ACTIVATION_BASE_URL);
224
+ authService.configureActivation({
225
+ // Fully qualified, one per link — the token is the only thing appended.
226
+ // AUTH_ACTIVATION_BASE_URL still works as an origin; see configureActivation.
227
+ activationUrl: process.env.AUTH_ACTIVATION_URL || null,
228
+ inviteUrl: process.env.AUTH_INVITE_URL || null,
229
+ baseUrl: process.env.AUTH_ACTIVATION_BASE_URL || null
230
+ });
202
231
  return _conn;
203
232
  })();
204
233
 
@@ -219,14 +248,41 @@ function attach() {
219
248
  var requiredEnv = [
220
249
  'ENCRYPTION_KEY',
221
250
  'AUTH_JWT_SECRET',
222
- 'AUTH_ACTIVATION_BASE_URL',
251
+ // NOT listed literally: the activation link can be given either as
252
+ // AUTH_ACTIVATION_URL (fully qualified, preferred) or as the older
253
+ // AUTH_ACTIVATION_BASE_URL (an origin). checkEnv takes a list of names and
254
+ // demands every one, which would refuse to boot an install that configured
255
+ // the new form. The getter below asks for whichever is actually in use.
223
256
  'AUTH_PORT',
224
257
  'AUTH_DB_NAME', // auth db (api reaches it via attach())
225
- 'AUTH_DB_CONNECTION_INFO_ENCRYPTED', // auth DB login (for attach())
226
- 'AUTH_EXT_MIGRATIONS_DIR', // migrations run via migrate:up
258
+ 'XEPLR_AUTH_MIGRATIONS', // migrations run via migrate:up
227
259
  ];
228
260
 
261
+ // Read at ACCESS time so it reflects the .env the app has already loaded — the
262
+ // same reason @xeplr/email's requiredEnv is a getter. Names the form the
263
+ // install is actually using, and asks for the new one when neither is set.
264
+ Object.defineProperty(requiredEnv, 'activationLinkVar', {
265
+ enumerable: false,
266
+ get: function () {
267
+ return process.env.AUTH_ACTIVATION_BASE_URL && !process.env.AUTH_ACTIVATION_URL
268
+ ? 'AUTH_ACTIVATION_BASE_URL'
269
+ : 'AUTH_ACTIVATION_URL';
270
+ }
271
+ });
272
+
273
+ // NOT in requiredEnv, deliberately: AUTH_DB_CONNECTION_INFO_ENCRYPTED is now an
274
+ // OVERRIDE. The connection normally comes from the shared XEPLR_DB_CONNECTION,
275
+ // so demanding the auth-specific name would fail a correctly configured
276
+ // install. Missing-ness is caught at the point of use by resolveDbConnection,
277
+ // which names both variables.
278
+ //
279
+ // AUTH_DB_NAME stays required and stays auth's own — the DATABASE is a
280
+ // different question from the SERVER, and services do not share one.
281
+
229
282
  module.exports = {
283
+ // Hand in @xeplr-workflow/api's resumeByKey to release a step that is
284
+ // waiting for someone to activate. Omitted, activation just activates.
285
+ configureWorkflowResume: authService.configureWorkflowResume,
230
286
  requiredEnv,
231
287
  init,
232
288
  attach,
package/lib/authRouter.js CHANGED
@@ -48,7 +48,10 @@ function createAuthRouter(options = {}) {
48
48
  return res.status(400).json({ error: 'Activation token is required' });
49
49
  }
50
50
 
51
- var result = await activate(token);
51
+ // Carried in the activation LINK and handed straight back — see
52
+ // authService.register. If a workflow is waiting on this person, this is
53
+ // the key that releases it.
54
+ var result = await activate(token, { workflowKey: req.query.workflowKey });
52
55
  res.json({ message: 'Account activated successfully', user: result });
53
56
  } catch (err) {
54
57
  logError(req, err);
@@ -7,13 +7,92 @@ const { getUserAccess, clearUserAccess } = require('./accessService');
7
7
  const { createSession, destroyAllUserSessions } = require('./sessionService');
8
8
  const hooks = require('./hooks');
9
9
 
10
- let _activationBaseUrl = null;
10
+ // WHERE THE LINKS IN OUR EMAILS POINT.
11
+ //
12
+ // FULLY QUALIFIED, one per link, because these strings do not only end up in a
13
+ // string concatenation here — they are handed to the email/template layer,
14
+ // where the token can travel as a variable but the URL cannot. A template that
15
+ // has to know to append '/auth/activate' to something called a "base" is a
16
+ // template that knows about this package's routing.
17
+ //
18
+ // The old AUTH_ACTIVATION_BASE_URL still works: given an origin, the default
19
+ // path for each link is appended, exactly as before. It served both the
20
+ // activation and the invite page, which is precisely why one fully-qualified
21
+ // value could not replace it.
22
+ var _links = { activationUrl: null, inviteUrl: null, baseUrl: null };
23
+
24
+ var DEFAULT_PATHS = { activation: '/auth/activate', invite: '/auth/accept-invite' };
25
+
26
+ // RESUMING A WORKFLOW ON ACTIVATION.
27
+ //
28
+ // A registration can be one step of a longer process — "invite them, wait for
29
+ // them to activate, then grant the roles" — and a workflow step that waits is
30
+ // released by its resumeKey (@xeplr-workflow/api's resumeByKey).
31
+ //
32
+ // INJECTED, not required. This package sits underneath every product here and
33
+ // must not depend on one of them; a host that runs workflows hands the
34
+ // function in at boot, and an install without workflow simply never sets it
35
+ // and never sees the feature. Same shape as configureEmail above.
36
+ var _resumeWorkflowByKey = null;
37
+
38
+ function configureWorkflowResume(fn) {
39
+ _resumeWorkflowByKey = typeof fn === 'function' ? fn : null;
40
+ }
41
+
42
+ /**
43
+ * Best-effort, and deliberately so.
44
+ *
45
+ * The account IS activated by the time this runs. If the workflow cannot be
46
+ * resumed — no such key, the run already moved on, workflow is down — that is
47
+ * a problem with the workflow, not with the person who just clicked the link
48
+ * in their email. Failing the activation over it would lock them out of an
49
+ * account that is already open.
50
+ */
51
+ async function resumeWorkflow(workflowKey, output) {
52
+ if (!workflowKey || !_resumeWorkflowByKey) return null;
53
+ try {
54
+ return await _resumeWorkflowByKey(workflowKey, output);
55
+ } catch (err) {
56
+ // Through @xeplr/logs, which is what routes an error onto the host's
57
+ // problem list (see xeplr-bi's errorEvents) — a workflow that silently
58
+ // never resumed is exactly the kind of thing nobody goes looking for.
59
+ require('@xeplr/logs').createLogger('auth')
60
+ .error('could not resume workflow "' + workflowKey + '": ' + err.message);
61
+ return null;
62
+ }
63
+ }
11
64
 
12
- function configureActivation(baseUrl) {
13
- _activationBaseUrl = baseUrl;
65
+ /**
66
+ * @param {string|object} config - a legacy origin string, or
67
+ * { activationUrl, inviteUrl, baseUrl }.
68
+ */
69
+ function configureActivation(config) {
70
+ if (typeof config === 'string') { _links.baseUrl = config; return; }
71
+ if (!config) return;
72
+ if (config.activationUrl !== undefined) _links.activationUrl = config.activationUrl;
73
+ if (config.inviteUrl !== undefined) _links.inviteUrl = config.inviteUrl;
74
+ if (config.baseUrl !== undefined) _links.baseUrl = config.baseUrl;
14
75
  }
15
76
 
16
- async function register({ email, phoneNumber, name, password, knex }) {
77
+ function linkFor(kind) {
78
+ var explicit = kind === 'activation'
79
+ ? (_links.activationUrl || process.env.AUTH_ACTIVATION_URL)
80
+ : (_links.inviteUrl || process.env.AUTH_INVITE_URL);
81
+ if (explicit) return explicit;
82
+
83
+ var base = _links.baseUrl || process.env.AUTH_ACTIVATION_BASE_URL;
84
+ if (!base) return null;
85
+ // Trailing slashes are the most common way a hand-edited .env produces a
86
+ // double slash in a link somebody then reports as broken.
87
+ return String(base).replace(/\/+$/, '') + DEFAULT_PATHS[kind];
88
+ }
89
+
90
+ /** Append the token, respecting a URL that already carries a query string. */
91
+ function withToken(url, token) {
92
+ return url + (url.indexOf('?') === -1 ? '?' : '&') + 'token=' + token;
93
+ }
94
+
95
+ async function register({ email, phoneNumber, name, password, workflowKey, knex }) {
17
96
  const UserM = knex ? User.bindKnex(knex) : User;
18
97
  const normalized = normalizeEmail(email);
19
98
 
@@ -47,12 +126,17 @@ async function register({ email, phoneNumber, name, password, knex }) {
47
126
  });
48
127
 
49
128
  // Send activation email
50
- const baseUrl = _activationBaseUrl || process.env.AUTH_ACTIVATION_BASE_URL;
51
- if (!baseUrl) {
52
- throw new Error('ACTIVATION_BASE_URL is required. Cannot register without email activation.');
129
+ const activationUrl = linkFor('activation');
130
+ if (!activationUrl) {
131
+ throw new Error('AUTH_ACTIVATION_URL is required (the full address of your activation page, ' +
132
+ 'e.g. http://localhost:19100/auth/activate). Cannot register without email activation.');
53
133
  }
54
134
 
55
- const activationLink = baseUrl + '/auth/activate?token=' + activationToken;
135
+ // The key rides along in the link so it comes back to us when the user
136
+ // clicks — there is nowhere else to keep it: activation happens in a
137
+ // different browser, days later, with no session.
138
+ var activationLink = withToken(activationUrl, activationToken);
139
+ if (workflowKey) activationLink += '&workflowKey=' + encodeURIComponent(workflowKey);
56
140
  const html = '<h2>Activate your account</h2>'
57
141
  + '<p>Hi ' + (name || 'there') + ',</p>'
58
142
  + '<p>Click below to activate your account:</p>'
@@ -72,16 +156,29 @@ async function register({ email, phoneNumber, name, password, knex }) {
72
156
  };
73
157
  }
74
158
 
75
- async function activate(token) {
159
+ async function activate(token, options) {
160
+ var workflowKey = options && options.workflowKey;
161
+ var log = require('@xeplr/logs').createLogger('auth');
162
+
163
+ // THE TOKEN IS NEVER LOGGED. It is a bearer credential — anyone holding the
164
+ // string can activate that account — and a log line is the one place it
165
+ // would sit in plain text long after the token itself was nulled. That a
166
+ // token arrived, and how it ended, is what is worth recording.
167
+ log.info('activation: token received' + (workflowKey ? ', workflowKey "' + workflowKey + '"' : ', no workflowKey'));
168
+
76
169
  const user = await User.query().findOne({ activationToken: token });
77
170
  if (!user) {
171
+ log.warn('activation: no user holds that token — expired, already used, or wrong link');
78
172
  throw new Error('Invalid activation token');
79
173
  }
80
174
 
81
175
  if (user.isActivated) {
176
+ log.info('activation: ' + user.email + ' is already activated — nothing to do');
82
177
  throw new Error('Account already activated');
83
178
  }
84
179
 
180
+ log.info('activation: activating ' + user.email);
181
+
85
182
  const now = formatDbDateTime();
86
183
  await User.query().findById(user.id).patch({
87
184
  isActivated: true,
@@ -90,8 +187,23 @@ async function activate(token) {
90
187
  recordModifiedDate: now
91
188
  });
92
189
 
190
+ log.important('activation: ' + user.email + ' activated');
93
191
  await hooks.fire('user', 'update', user.id);
94
192
 
193
+ // AFTER the patch, never before: a workflow released by this step may go on
194
+ // to grant roles or send a welcome mail, and it must not act on an account
195
+ // that has not actually been activated yet.
196
+ if (workflowKey) {
197
+ log.info('activation: resuming workflow "' + workflowKey + '"');
198
+ var resumed = await resumeWorkflow(workflowKey,
199
+ { userId: user.id, email: user.email, name: user.name });
200
+ if (resumed) {
201
+ log.important('activation: workflow "' + workflowKey + '" resumed (run ' +
202
+ (resumed.runId || '?') + ', step ' + (resumed.stepKey || '?') + ')');
203
+ }
204
+ // The failure path already logged its own reason inside resumeWorkflow.
205
+ }
206
+
95
207
  return { id: user.id, email: user.email, name: user.name };
96
208
  }
97
209
 
@@ -194,12 +306,13 @@ async function invite({ email, name, phoneNumber, invitedBy, inviterName, appNam
194
306
  recordModifiedBy: inviter
195
307
  });
196
308
 
197
- const baseUrl = _activationBaseUrl || process.env.AUTH_ACTIVATION_BASE_URL;
198
- if (!baseUrl) {
199
- throw new Error('ACTIVATION_BASE_URL is required to send invite emails.');
309
+ const inviteUrl = linkFor('invite');
310
+ if (!inviteUrl) {
311
+ throw new Error('AUTH_INVITE_URL is required to send invite emails (the full address of your ' +
312
+ 'accept-invite page, e.g. http://localhost:19100/auth/accept-invite).');
200
313
  }
201
314
 
202
- const acceptLink = baseUrl + '/auth/accept-invite?token=' + activationToken;
315
+ const acceptLink = withToken(inviteUrl, activationToken);
203
316
  const app = appName || 'the app';
204
317
  const subject = 'You have been invited to ' + app;
205
318
  const html = '<h2>You have been invited</h2>'
@@ -494,6 +607,7 @@ async function changePassword(userId, { oldPassword, newPassword }) {
494
607
 
495
608
  module.exports = {
496
609
  configureActivation,
610
+ configureWorkflowResume,
497
611
  configureEmail,
498
612
  register,
499
613
  activate,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xeplr/auth",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Pluggable authentication service: register, login, forgot/reset password, RBAC, multi-tenancy",
5
5
  "main": "index.js",
6
6
  "files": [