@vsaas/loopback 10.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/LICENSE +25 -0
- package/README.md +91 -0
- package/common/models/README.md +109 -0
- package/common/models/access-token.json +37 -0
- package/common/models/acl.json +17 -0
- package/common/models/application.json +130 -0
- package/common/models/change.json +25 -0
- package/common/models/checkpoint.json +14 -0
- package/common/models/email.json +11 -0
- package/common/models/key-value-model.json +4 -0
- package/common/models/role-mapping.json +26 -0
- package/common/models/role.json +30 -0
- package/common/models/scope.json +14 -0
- package/common/models/user.json +118 -0
- package/dist/_virtual/_rolldown/runtime.cjs +32 -0
- package/dist/common/models/access-token.cjs +144 -0
- package/dist/common/models/access-token2.cjs +43 -0
- package/dist/common/models/acl.cjs +428 -0
- package/dist/common/models/acl2.cjs +27 -0
- package/dist/common/models/application.cjs +100 -0
- package/dist/common/models/application2.cjs +118 -0
- package/dist/common/models/change.cjs +404 -0
- package/dist/common/models/change2.cjs +25 -0
- package/dist/common/models/checkpoint.cjs +43 -0
- package/dist/common/models/checkpoint2.cjs +18 -0
- package/dist/common/models/email.cjs +18 -0
- package/dist/common/models/email2.cjs +30 -0
- package/dist/common/models/key-value-model.cjs +140 -0
- package/dist/common/models/key-value-model2.cjs +14 -0
- package/dist/common/models/role-mapping.cjs +57 -0
- package/dist/common/models/role-mapping2.cjs +34 -0
- package/dist/common/models/role.cjs +396 -0
- package/dist/common/models/role2.cjs +38 -0
- package/dist/common/models/scope.cjs +30 -0
- package/dist/common/models/scope2.cjs +21 -0
- package/dist/common/models/user.cjs +810 -0
- package/dist/common/models/user2.cjs +118 -0
- package/dist/index.cjs +16 -0
- package/dist/lib/access-context.cjs +228 -0
- package/dist/lib/application.cjs +450 -0
- package/dist/lib/builtin-models.cjs +60 -0
- package/dist/lib/configure-shared-methods.cjs +41 -0
- package/dist/lib/connectors/base-connector.cjs +23 -0
- package/dist/lib/connectors/mail-direct-transport.cjs +375 -0
- package/dist/lib/connectors/mail-stub-transport.cjs +86 -0
- package/dist/lib/connectors/mail.cjs +128 -0
- package/dist/lib/connectors/memory.cjs +19 -0
- package/dist/lib/current-context.cjs +22 -0
- package/dist/lib/globalize.cjs +29 -0
- package/dist/lib/loopback.cjs +313 -0
- package/dist/lib/model.cjs +1009 -0
- package/dist/lib/persisted-model.cjs +1835 -0
- package/dist/lib/registry.cjs +291 -0
- package/dist/lib/runtime.cjs +25 -0
- package/dist/lib/server-app.cjs +231 -0
- package/dist/lib/utils.cjs +154 -0
- package/dist/package.cjs +124 -0
- package/dist/server/middleware/context.cjs +7 -0
- package/dist/server/middleware/error-handler.cjs +6 -0
- package/dist/server/middleware/favicon.cjs +13 -0
- package/dist/server/middleware/rest.cjs +44 -0
- package/dist/server/middleware/static.cjs +14 -0
- package/dist/server/middleware/status.cjs +28 -0
- package/dist/server/middleware/token.cjs +66 -0
- package/dist/server/middleware/url-not-found.cjs +20 -0
- package/favicon.ico +0 -0
- package/package.json +121 -0
- package/templates/reset-form.ejs +3 -0
- package/templates/verify.ejs +9 -0
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const require_runtime$1 = require("../../_virtual/_rolldown/runtime.cjs");
|
|
3
|
+
const require_lib_globalize = require("../../lib/globalize.cjs");
|
|
4
|
+
const require_lib_runtime = require("../../lib/runtime.cjs");
|
|
5
|
+
const require_lib_utils = require("../../lib/utils.cjs");
|
|
6
|
+
//#region src/common/models/user.ts
|
|
7
|
+
var require_user = /* @__PURE__ */ require_runtime$1.__commonJSMin(((exports, module) => {
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const g = require_lib_globalize;
|
|
10
|
+
const runtime = require_lib_runtime;
|
|
11
|
+
require("ejs");
|
|
12
|
+
require("fs");
|
|
13
|
+
const isEmail = require("isemail");
|
|
14
|
+
const utils = require_lib_utils;
|
|
15
|
+
const qs = require("querystring");
|
|
16
|
+
const crypto = require("crypto");
|
|
17
|
+
const assert = require("assert");
|
|
18
|
+
const SALT_WORK_FACTOR = 10;
|
|
19
|
+
const MAX_PASSWORD_LENGTH = 72;
|
|
20
|
+
let bcrypt;
|
|
21
|
+
try {
|
|
22
|
+
bcrypt = require("bcrypt");
|
|
23
|
+
if (bcrypt && typeof bcrypt.compare !== "function") bcrypt = require("bcryptjs");
|
|
24
|
+
} catch (err) {
|
|
25
|
+
bcrypt = require("bcryptjs");
|
|
26
|
+
}
|
|
27
|
+
const DEFAULT_TTL = 1209600;
|
|
28
|
+
const DEFAULT_RESET_PW_TTL = 900;
|
|
29
|
+
const DEFAULT_MAX_TTL = 31556926;
|
|
30
|
+
const debug = require("debug")("loopback:user");
|
|
31
|
+
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
32
|
+
function configureUser(User) {
|
|
33
|
+
User.prototype.createAccessToken = function(ttl, options, cb) {
|
|
34
|
+
if (cb === void 0 && typeof options === "function") {
|
|
35
|
+
cb = options;
|
|
36
|
+
options = void 0;
|
|
37
|
+
}
|
|
38
|
+
cb = cb || utils.createPromiseCallback();
|
|
39
|
+
let tokenData;
|
|
40
|
+
if (typeof ttl !== "object") tokenData = { ttl };
|
|
41
|
+
else if (options) tokenData = ttl;
|
|
42
|
+
else tokenData = {};
|
|
43
|
+
const userSettings = this.constructor.settings;
|
|
44
|
+
tokenData.ttl = Math.min(tokenData.ttl || userSettings.ttl, userSettings.maxTTL);
|
|
45
|
+
this.accessTokens.create(tokenData, options, cb);
|
|
46
|
+
return cb.promise;
|
|
47
|
+
};
|
|
48
|
+
function splitPrincipal(name, realmDelimiter) {
|
|
49
|
+
const parts = [null, name];
|
|
50
|
+
if (!realmDelimiter) return parts;
|
|
51
|
+
const index = name.indexOf(realmDelimiter);
|
|
52
|
+
if (index !== -1) {
|
|
53
|
+
parts[0] = name.substring(0, index);
|
|
54
|
+
parts[1] = name.substring(index + realmDelimiter.length);
|
|
55
|
+
}
|
|
56
|
+
return parts;
|
|
57
|
+
}
|
|
58
|
+
User.normalizeCredentials = function(credentials, realmRequired, realmDelimiter) {
|
|
59
|
+
const query = {};
|
|
60
|
+
credentials = credentials || {};
|
|
61
|
+
if (!realmRequired) {
|
|
62
|
+
if (credentials.email) query.email = credentials.email;
|
|
63
|
+
else if (credentials.username) query.username = credentials.username;
|
|
64
|
+
} else {
|
|
65
|
+
if (credentials.realm) query.realm = credentials.realm;
|
|
66
|
+
let parts;
|
|
67
|
+
if (credentials.email) {
|
|
68
|
+
parts = splitPrincipal(credentials.email, realmDelimiter);
|
|
69
|
+
query.email = parts[1];
|
|
70
|
+
if (parts[0]) query.realm = parts[0];
|
|
71
|
+
} else if (credentials.username) {
|
|
72
|
+
parts = splitPrincipal(credentials.username, realmDelimiter);
|
|
73
|
+
query.username = parts[1];
|
|
74
|
+
if (parts[0]) query.realm = parts[0];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return query;
|
|
78
|
+
};
|
|
79
|
+
User.login = function(credentials, include, fn) {
|
|
80
|
+
const self = this;
|
|
81
|
+
if (typeof include === "function") {
|
|
82
|
+
fn = include;
|
|
83
|
+
include = void 0;
|
|
84
|
+
}
|
|
85
|
+
fn = fn || utils.createPromiseCallback();
|
|
86
|
+
include = include || "";
|
|
87
|
+
if (Array.isArray(include)) include = include.map(function(val) {
|
|
88
|
+
return val.toLowerCase();
|
|
89
|
+
});
|
|
90
|
+
else include = include.toLowerCase();
|
|
91
|
+
let realmDelimiter;
|
|
92
|
+
const realmRequired = !!(self.settings.realmRequired || self.settings.realmDelimiter);
|
|
93
|
+
if (realmRequired) realmDelimiter = self.settings.realmDelimiter;
|
|
94
|
+
const query = self.normalizeCredentials(credentials, realmRequired, realmDelimiter);
|
|
95
|
+
if (realmRequired) {
|
|
96
|
+
if (!query.realm) {
|
|
97
|
+
const err1 = new Error(g.f("{{realm}} is required"));
|
|
98
|
+
err1.statusCode = 400;
|
|
99
|
+
err1.code = "REALM_REQUIRED";
|
|
100
|
+
fn(err1);
|
|
101
|
+
return fn.promise;
|
|
102
|
+
} else if (typeof query.realm !== "string") {
|
|
103
|
+
const err5 = new Error(g.f("Invalid realm"));
|
|
104
|
+
err5.statusCode = 400;
|
|
105
|
+
err5.code = "INVALID_REALM";
|
|
106
|
+
fn(err5);
|
|
107
|
+
return fn.promise;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!query.email && !query.username) {
|
|
111
|
+
const err2 = new Error(g.f("{{username}} or {{email}} is required"));
|
|
112
|
+
err2.statusCode = 400;
|
|
113
|
+
err2.code = "USERNAME_EMAIL_REQUIRED";
|
|
114
|
+
fn(err2);
|
|
115
|
+
return fn.promise;
|
|
116
|
+
}
|
|
117
|
+
if (query.username && typeof query.username !== "string") {
|
|
118
|
+
const err3 = new Error(g.f("Invalid username"));
|
|
119
|
+
err3.statusCode = 400;
|
|
120
|
+
err3.code = "INVALID_USERNAME";
|
|
121
|
+
fn(err3);
|
|
122
|
+
return fn.promise;
|
|
123
|
+
} else if (query.email && typeof query.email !== "string") {
|
|
124
|
+
const err4 = new Error(g.f("Invalid email"));
|
|
125
|
+
err4.statusCode = 400;
|
|
126
|
+
err4.code = "INVALID_EMAIL";
|
|
127
|
+
fn(err4);
|
|
128
|
+
return fn.promise;
|
|
129
|
+
}
|
|
130
|
+
self.findOne({ where: query }, function(err, user) {
|
|
131
|
+
const defaultError = new Error(g.f("login failed"));
|
|
132
|
+
defaultError.statusCode = 401;
|
|
133
|
+
defaultError.code = "LOGIN_FAILED";
|
|
134
|
+
function tokenHandler(err, token) {
|
|
135
|
+
if (err) return fn(err);
|
|
136
|
+
if (Array.isArray(include) ? include.indexOf("user") !== -1 : include === "user") token.__data.user = user;
|
|
137
|
+
fn(err, token);
|
|
138
|
+
}
|
|
139
|
+
if (err) {
|
|
140
|
+
debug("An error is reported from User.findOne: %j", err);
|
|
141
|
+
fn(defaultError);
|
|
142
|
+
} else if (user) user.hasPassword(credentials.password, function(err, isMatch) {
|
|
143
|
+
if (err) {
|
|
144
|
+
debug("An error is reported from User.hasPassword: %j", err);
|
|
145
|
+
fn(defaultError);
|
|
146
|
+
} else if (isMatch) if (self.settings.emailVerificationRequired && !user.emailVerified) {
|
|
147
|
+
debug("User email has not been verified");
|
|
148
|
+
err = new Error(g.f("login failed as the email has not been verified"));
|
|
149
|
+
err.statusCode = 401;
|
|
150
|
+
err.code = "LOGIN_FAILED_EMAIL_NOT_VERIFIED";
|
|
151
|
+
err.details = { userId: user.id };
|
|
152
|
+
fn(err);
|
|
153
|
+
} else if (user.createAccessToken.length === 2) user.createAccessToken(credentials.ttl, tokenHandler);
|
|
154
|
+
else user.createAccessToken(credentials.ttl, credentials, tokenHandler);
|
|
155
|
+
else {
|
|
156
|
+
debug("The password is invalid for user %s", query.email || query.username);
|
|
157
|
+
fn(defaultError);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
else {
|
|
161
|
+
debug("No matching record is found for user %s", query.email || query.username);
|
|
162
|
+
fn(defaultError);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
return fn.promise;
|
|
166
|
+
};
|
|
167
|
+
User.logout = function(tokenId, fn) {
|
|
168
|
+
fn = fn || utils.createPromiseCallback();
|
|
169
|
+
let err;
|
|
170
|
+
if (!tokenId) {
|
|
171
|
+
err = new Error(g.f("{{accessToken}} is required to logout"));
|
|
172
|
+
err.statusCode = 401;
|
|
173
|
+
process.nextTick(fn, err);
|
|
174
|
+
return fn.promise;
|
|
175
|
+
}
|
|
176
|
+
this.relations.accessTokens.modelTo.destroyById(tokenId, function(err, info) {
|
|
177
|
+
if (err) fn(err);
|
|
178
|
+
else if ("count" in info && info.count === 0) {
|
|
179
|
+
err = new Error(g.f("Could not find {{accessToken}}"));
|
|
180
|
+
err.statusCode = 401;
|
|
181
|
+
fn(err);
|
|
182
|
+
} else fn();
|
|
183
|
+
});
|
|
184
|
+
return fn.promise;
|
|
185
|
+
};
|
|
186
|
+
User.observe("before delete", function(ctx, next) {
|
|
187
|
+
if (!ctx.Model.relations.accessTokens) return next();
|
|
188
|
+
const AccessToken = ctx.Model.relations.accessTokens.modelTo;
|
|
189
|
+
const pkName = ctx.Model.definition.idName() || "id";
|
|
190
|
+
ctx.Model.find({
|
|
191
|
+
where: ctx.where,
|
|
192
|
+
fields: [pkName]
|
|
193
|
+
}, function(err, list) {
|
|
194
|
+
if (err) return next(err);
|
|
195
|
+
const ids = new Array(list.length);
|
|
196
|
+
for (let i = 0; i < list.length; i++) ids[i] = list[i][pkName];
|
|
197
|
+
ctx.where = {};
|
|
198
|
+
ctx.where[pkName] = { inq: ids };
|
|
199
|
+
AccessToken.destroyAll({ userId: { inq: ids } }, next);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
User.prototype.hasPassword = function(plain, fn) {
|
|
203
|
+
fn = fn || utils.createPromiseCallback();
|
|
204
|
+
if (this.password && plain) bcrypt.compare(plain, this.password, function(err, isMatch) {
|
|
205
|
+
if (err) return fn(err);
|
|
206
|
+
fn(null, isMatch);
|
|
207
|
+
});
|
|
208
|
+
else fn(null, false);
|
|
209
|
+
return fn.promise;
|
|
210
|
+
};
|
|
211
|
+
User.changePassword = function(userId, oldPassword, newPassword, options, cb) {
|
|
212
|
+
if (cb === void 0 && typeof options === "function") {
|
|
213
|
+
cb = options;
|
|
214
|
+
options = void 0;
|
|
215
|
+
}
|
|
216
|
+
cb = cb || utils.createPromiseCallback();
|
|
217
|
+
this.findById(userId, options, (err, inst) => {
|
|
218
|
+
if (err) return cb(err);
|
|
219
|
+
if (!inst) {
|
|
220
|
+
const err = /* @__PURE__ */ new Error(`User ${userId} not found`);
|
|
221
|
+
Object.assign(err, {
|
|
222
|
+
code: "USER_NOT_FOUND",
|
|
223
|
+
statusCode: 401
|
|
224
|
+
});
|
|
225
|
+
return cb(err);
|
|
226
|
+
}
|
|
227
|
+
inst.changePassword(oldPassword, newPassword, options, cb);
|
|
228
|
+
});
|
|
229
|
+
return cb.promise;
|
|
230
|
+
};
|
|
231
|
+
User.prototype.changePassword = function(oldPassword, newPassword, options, cb) {
|
|
232
|
+
if (cb === void 0 && typeof options === "function") {
|
|
233
|
+
cb = options;
|
|
234
|
+
options = void 0;
|
|
235
|
+
}
|
|
236
|
+
cb = cb || utils.createPromiseCallback();
|
|
237
|
+
this.hasPassword(oldPassword, (err, isMatch) => {
|
|
238
|
+
if (err) return cb(err);
|
|
239
|
+
if (!isMatch) {
|
|
240
|
+
const err = /* @__PURE__ */ new Error("Invalid current password");
|
|
241
|
+
Object.assign(err, {
|
|
242
|
+
code: "INVALID_PASSWORD",
|
|
243
|
+
statusCode: 400
|
|
244
|
+
});
|
|
245
|
+
return cb(err);
|
|
246
|
+
}
|
|
247
|
+
this.setPassword(newPassword, options, cb);
|
|
248
|
+
});
|
|
249
|
+
return cb.promise;
|
|
250
|
+
};
|
|
251
|
+
User.setPassword = function(userId, newPassword, options, cb) {
|
|
252
|
+
assert(userId != null && userId !== "", "userId is a required argument");
|
|
253
|
+
assert(!!newPassword, "newPassword is a required argument");
|
|
254
|
+
if (cb === void 0 && typeof options === "function") {
|
|
255
|
+
cb = options;
|
|
256
|
+
options = void 0;
|
|
257
|
+
}
|
|
258
|
+
cb = cb || utils.createPromiseCallback();
|
|
259
|
+
this.findById(userId, options, (err, inst) => {
|
|
260
|
+
if (err) return cb(err);
|
|
261
|
+
if (!inst) {
|
|
262
|
+
const err = /* @__PURE__ */ new Error(`User ${userId} not found`);
|
|
263
|
+
Object.assign(err, {
|
|
264
|
+
code: "USER_NOT_FOUND",
|
|
265
|
+
statusCode: 401
|
|
266
|
+
});
|
|
267
|
+
return cb(err);
|
|
268
|
+
}
|
|
269
|
+
inst.setPassword(newPassword, options, cb);
|
|
270
|
+
});
|
|
271
|
+
return cb.promise;
|
|
272
|
+
};
|
|
273
|
+
User.prototype.setPassword = function(newPassword, options, cb) {
|
|
274
|
+
assert(!!newPassword, "newPassword is a required argument");
|
|
275
|
+
if (cb === void 0 && typeof options === "function") {
|
|
276
|
+
cb = options;
|
|
277
|
+
options = void 0;
|
|
278
|
+
}
|
|
279
|
+
cb = cb || utils.createPromiseCallback();
|
|
280
|
+
try {
|
|
281
|
+
this.constructor.validatePassword(newPassword);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
cb(err);
|
|
284
|
+
return cb.promise;
|
|
285
|
+
}
|
|
286
|
+
options = Object.assign({}, options);
|
|
287
|
+
options.setPassword = true;
|
|
288
|
+
const delta = { password: newPassword };
|
|
289
|
+
this.patchAttributes(delta, options, function(err) {
|
|
290
|
+
cb(err);
|
|
291
|
+
});
|
|
292
|
+
return cb.promise;
|
|
293
|
+
};
|
|
294
|
+
User.getVerifyOptions = function() {
|
|
295
|
+
return Object.assign({}, this.settings.verifyOptions || {
|
|
296
|
+
type: "email",
|
|
297
|
+
from: "noreply@example.com"
|
|
298
|
+
});
|
|
299
|
+
};
|
|
300
|
+
User.prototype.verify = function(verifyOptions, options, cb) {
|
|
301
|
+
if (cb === void 0 && typeof options === "function") {
|
|
302
|
+
cb = options;
|
|
303
|
+
options = void 0;
|
|
304
|
+
}
|
|
305
|
+
cb = cb || utils.createPromiseCallback();
|
|
306
|
+
const user = this;
|
|
307
|
+
const userModel = this.constructor;
|
|
308
|
+
const loopback = runtime.loopback;
|
|
309
|
+
const registry = userModel.registry;
|
|
310
|
+
verifyOptions = Object.assign({}, verifyOptions);
|
|
311
|
+
assert(typeof verifyOptions === "object", "verifyOptions object param required when calling user.verify()");
|
|
312
|
+
verifyOptions = Object.assign({}, verifyOptions);
|
|
313
|
+
verifyOptions.templateFn = verifyOptions.templateFn || createVerificationEmailBody;
|
|
314
|
+
verifyOptions.generateVerificationToken = verifyOptions.generateVerificationToken || User.generateVerificationToken;
|
|
315
|
+
verifyOptions.mailer = verifyOptions.mailer || userModel.email || registry.getModelByType(loopback.Email);
|
|
316
|
+
const pkName = userModel.definition.idName() || "id";
|
|
317
|
+
verifyOptions.redirect = verifyOptions.redirect || "/";
|
|
318
|
+
const defaultTemplate = path.join(__dirname, "..", "..", "..", "templates", "verify.ejs");
|
|
319
|
+
verifyOptions.template = path.resolve(verifyOptions.template || defaultTemplate);
|
|
320
|
+
verifyOptions.user = user;
|
|
321
|
+
verifyOptions.protocol = verifyOptions.protocol || "http";
|
|
322
|
+
const app = userModel.app;
|
|
323
|
+
verifyOptions.host = verifyOptions.host || app && app.get("host") || "localhost";
|
|
324
|
+
verifyOptions.port = verifyOptions.port || app && app.get("port") || 3e3;
|
|
325
|
+
verifyOptions.restApiRoot = verifyOptions.restApiRoot || app && app.get("restApiRoot") || "/api";
|
|
326
|
+
const displayPort = verifyOptions.protocol === "http" && verifyOptions.port == "80" || verifyOptions.protocol === "https" && verifyOptions.port == "443" ? "" : ":" + verifyOptions.port;
|
|
327
|
+
if (!verifyOptions.verifyHref) {
|
|
328
|
+
const confirmMethod = userModel.sharedClass.findMethodByName("confirm");
|
|
329
|
+
if (!confirmMethod) throw new Error("Cannot build user verification URL, the default confirm method is not public. Please provide the URL in verifyOptions.verifyHref.");
|
|
330
|
+
const urlPath = joinUrlPath(verifyOptions.restApiRoot, userModel.http.path, confirmMethod.http.path);
|
|
331
|
+
verifyOptions.verifyHref = verifyOptions.protocol + "://" + verifyOptions.host + displayPort + urlPath + "?" + qs.stringify({
|
|
332
|
+
uid: "" + verifyOptions.user[pkName],
|
|
333
|
+
redirect: verifyOptions.redirect
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
verifyOptions.to = verifyOptions.to || user.email;
|
|
337
|
+
verifyOptions.subject = verifyOptions.subject || g.f("Thanks for Registering");
|
|
338
|
+
verifyOptions.headers = verifyOptions.headers || {};
|
|
339
|
+
assertVerifyOptions(verifyOptions);
|
|
340
|
+
const tokenGenerator = verifyOptions.generateVerificationToken;
|
|
341
|
+
if (tokenGenerator.length == 3) tokenGenerator(user, options, addTokenToUserAndSave);
|
|
342
|
+
else tokenGenerator(user, addTokenToUserAndSave);
|
|
343
|
+
function addTokenToUserAndSave(err, token) {
|
|
344
|
+
if (err) return cb(err);
|
|
345
|
+
user.verificationToken = token;
|
|
346
|
+
user.save(options, function(err) {
|
|
347
|
+
if (err) return cb(err);
|
|
348
|
+
sendEmail(user);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
function sendEmail(user) {
|
|
352
|
+
verifyOptions.verifyHref += verifyOptions.verifyHref.indexOf("?") === -1 ? "?" : "&";
|
|
353
|
+
verifyOptions.verifyHref += "token=" + user.verificationToken;
|
|
354
|
+
verifyOptions.verificationToken = user.verificationToken;
|
|
355
|
+
verifyOptions.text = verifyOptions.text || g.f("Please verify your email by opening this link in a web browser:\n %s", verifyOptions.verifyHref);
|
|
356
|
+
verifyOptions.text = verifyOptions.text.replace(/\{href\}/g, verifyOptions.verifyHref);
|
|
357
|
+
const templateFn = verifyOptions.templateFn;
|
|
358
|
+
if (templateFn.length == 3) templateFn(verifyOptions, options, setHtmlContentAndSend);
|
|
359
|
+
else templateFn(verifyOptions, setHtmlContentAndSend);
|
|
360
|
+
function setHtmlContentAndSend(err, html) {
|
|
361
|
+
if (err) return cb(err);
|
|
362
|
+
verifyOptions.html = html;
|
|
363
|
+
delete verifyOptions.template;
|
|
364
|
+
const Email = verifyOptions.mailer;
|
|
365
|
+
if (Email.send.length == 3) Email.send(verifyOptions, options, handleAfterSend);
|
|
366
|
+
else Email.send(verifyOptions, handleAfterSend);
|
|
367
|
+
function handleAfterSend(err, email) {
|
|
368
|
+
if (err) return cb(err);
|
|
369
|
+
cb(null, {
|
|
370
|
+
email,
|
|
371
|
+
token: user.verificationToken,
|
|
372
|
+
uid: user[pkName]
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return cb.promise;
|
|
378
|
+
};
|
|
379
|
+
function assertVerifyOptions(verifyOptions) {
|
|
380
|
+
assert(verifyOptions.type, "You must supply a verification type (verifyOptions.type)");
|
|
381
|
+
assert(verifyOptions.type === "email", "Unsupported verification type");
|
|
382
|
+
assert(verifyOptions.to, "Must include verifyOptions.to when calling user.verify() or the user must have an email property");
|
|
383
|
+
assert(verifyOptions.from, "Must include verifyOptions.from when calling user.verify()");
|
|
384
|
+
assert(typeof verifyOptions.templateFn === "function", "templateFn must be a function");
|
|
385
|
+
assert(typeof verifyOptions.generateVerificationToken === "function", "generateVerificationToken must be a function");
|
|
386
|
+
assert(verifyOptions.mailer, "A mailer function must be provided");
|
|
387
|
+
assert(typeof verifyOptions.mailer.send === "function", "mailer.send must be a function ");
|
|
388
|
+
}
|
|
389
|
+
function createVerificationEmailBody(verifyOptions, options, cb) {
|
|
390
|
+
cb(null, runtime.loopback.template(verifyOptions.template)(verifyOptions));
|
|
391
|
+
}
|
|
392
|
+
User.generateVerificationToken = function(user, options, cb) {
|
|
393
|
+
crypto.randomBytes(64, function(err, buf) {
|
|
394
|
+
cb(err, buf && buf.toString("hex"));
|
|
395
|
+
});
|
|
396
|
+
};
|
|
397
|
+
User.confirm = function(uid, token, redirect, fn) {
|
|
398
|
+
fn = fn || utils.createPromiseCallback();
|
|
399
|
+
this.findById(uid, function(err, user) {
|
|
400
|
+
if (err) fn(err);
|
|
401
|
+
else if (user && user.verificationToken === token) {
|
|
402
|
+
user.verificationToken = null;
|
|
403
|
+
user.emailVerified = true;
|
|
404
|
+
user.save(function(err) {
|
|
405
|
+
if (err) fn(err);
|
|
406
|
+
else fn();
|
|
407
|
+
});
|
|
408
|
+
} else {
|
|
409
|
+
if (user) {
|
|
410
|
+
err = new Error(g.f("Invalid token: %s", token));
|
|
411
|
+
err.statusCode = 400;
|
|
412
|
+
err.code = "INVALID_TOKEN";
|
|
413
|
+
} else {
|
|
414
|
+
err = new Error(g.f("User not found: %s", uid));
|
|
415
|
+
err.statusCode = 404;
|
|
416
|
+
err.code = "USER_NOT_FOUND";
|
|
417
|
+
}
|
|
418
|
+
fn(err);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
return fn.promise;
|
|
422
|
+
};
|
|
423
|
+
User.resetPassword = function(options, cb) {
|
|
424
|
+
cb = cb || utils.createPromiseCallback();
|
|
425
|
+
const UserModel = this;
|
|
426
|
+
const ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL;
|
|
427
|
+
options = options || {};
|
|
428
|
+
if (typeof options.email !== "string") {
|
|
429
|
+
const err = new Error(g.f("Email is required"));
|
|
430
|
+
err.statusCode = 400;
|
|
431
|
+
err.code = "EMAIL_REQUIRED";
|
|
432
|
+
cb(err);
|
|
433
|
+
return cb.promise;
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
if (options.password) UserModel.validatePassword(options.password);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
return cb(err);
|
|
439
|
+
}
|
|
440
|
+
const where = { email: options.email };
|
|
441
|
+
if (options.realm) where.realm = options.realm;
|
|
442
|
+
UserModel.findOne({ where }, function(err, user) {
|
|
443
|
+
if (err) return cb(err);
|
|
444
|
+
if (!user) {
|
|
445
|
+
err = new Error(g.f("Email not found"));
|
|
446
|
+
err.statusCode = 404;
|
|
447
|
+
err.code = "EMAIL_NOT_FOUND";
|
|
448
|
+
return cb(err);
|
|
449
|
+
}
|
|
450
|
+
if (UserModel.settings.emailVerificationRequired && !user.emailVerified) {
|
|
451
|
+
err = new Error(g.f("Email has not been verified"));
|
|
452
|
+
err.statusCode = 401;
|
|
453
|
+
err.code = "RESET_FAILED_EMAIL_NOT_VERIFIED";
|
|
454
|
+
return cb(err);
|
|
455
|
+
}
|
|
456
|
+
if (UserModel.settings.restrictResetPasswordTokenScope) {
|
|
457
|
+
const tokenData = {
|
|
458
|
+
ttl,
|
|
459
|
+
scopes: ["reset-password"]
|
|
460
|
+
};
|
|
461
|
+
user.createAccessToken(tokenData, options, onTokenCreated);
|
|
462
|
+
} else user.createAccessToken(ttl, onTokenCreated);
|
|
463
|
+
function onTokenCreated(err, accessToken) {
|
|
464
|
+
if (err) return cb(err);
|
|
465
|
+
cb();
|
|
466
|
+
UserModel.emit("resetPasswordRequest", {
|
|
467
|
+
email: options.email,
|
|
468
|
+
accessToken,
|
|
469
|
+
user,
|
|
470
|
+
options
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
return cb.promise;
|
|
475
|
+
};
|
|
476
|
+
User.hashPassword = function(plain) {
|
|
477
|
+
this.validatePassword(plain);
|
|
478
|
+
const salt = bcrypt.genSaltSync(this.settings.saltWorkFactor || SALT_WORK_FACTOR);
|
|
479
|
+
return bcrypt.hashSync(plain, salt);
|
|
480
|
+
};
|
|
481
|
+
User.validatePassword = function(plain) {
|
|
482
|
+
let err;
|
|
483
|
+
if (!plain || typeof plain !== "string") {
|
|
484
|
+
err = new Error(g.f("Invalid password."));
|
|
485
|
+
err.code = "INVALID_PASSWORD";
|
|
486
|
+
err.statusCode = 422;
|
|
487
|
+
throw err;
|
|
488
|
+
}
|
|
489
|
+
const len = Buffer.byteLength(plain, "utf8");
|
|
490
|
+
if (len > MAX_PASSWORD_LENGTH) {
|
|
491
|
+
err = new Error(g.f("The password entered was too long. Max length is %d (entered %d)", MAX_PASSWORD_LENGTH, len));
|
|
492
|
+
err.code = "PASSWORD_TOO_LONG";
|
|
493
|
+
err.statusCode = 422;
|
|
494
|
+
throw err;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
User._invalidateAccessTokensOfUsers = function(userIds, options, cb) {
|
|
498
|
+
if (typeof options === "function" && cb === void 0) {
|
|
499
|
+
cb = options;
|
|
500
|
+
options = {};
|
|
501
|
+
}
|
|
502
|
+
if (!Array.isArray(userIds) || !userIds.length) return process.nextTick(cb);
|
|
503
|
+
const accessTokenRelation = this.relations.accessTokens;
|
|
504
|
+
if (!accessTokenRelation) return process.nextTick(cb);
|
|
505
|
+
const AccessToken = accessTokenRelation.modelTo;
|
|
506
|
+
const query = { userId: { inq: userIds } };
|
|
507
|
+
const tokenPK = AccessToken.definition.idName() || "id";
|
|
508
|
+
if (options.accessToken && tokenPK in options.accessToken) query[tokenPK] = { neq: options.accessToken[tokenPK] };
|
|
509
|
+
const relatedUser = AccessToken.relations.user;
|
|
510
|
+
if (relatedUser && relatedUser.polymorphic && !relatedUser.modelTo) query.principalType = this.modelName;
|
|
511
|
+
AccessToken.deleteAll(query, options, cb);
|
|
512
|
+
};
|
|
513
|
+
User.setup = function() {
|
|
514
|
+
User.base.setup.call(this);
|
|
515
|
+
const UserModel = this;
|
|
516
|
+
const loopback = runtime.loopback;
|
|
517
|
+
this.settings.maxTTL = this.settings.maxTTL || DEFAULT_MAX_TTL;
|
|
518
|
+
this.settings.ttl = this.settings.ttl || DEFAULT_TTL;
|
|
519
|
+
UserModel.setter.email = function(value) {
|
|
520
|
+
if (!UserModel.settings.caseSensitiveEmail && typeof value === "string") this.$email = value.toLowerCase();
|
|
521
|
+
else this.$email = value;
|
|
522
|
+
};
|
|
523
|
+
UserModel.setter.password = function(plain) {
|
|
524
|
+
if (typeof plain !== "string") return;
|
|
525
|
+
if ((plain.indexOf("$2a$") === 0 || plain.indexOf("$2b$") === 0) && plain.length === 60) this.$password = plain;
|
|
526
|
+
else this.$password = this.constructor.hashPassword(plain);
|
|
527
|
+
};
|
|
528
|
+
UserModel.beforeRemote("create", function(ctx, user, next) {
|
|
529
|
+
const body = ctx.req.body;
|
|
530
|
+
if (body && body.emailVerified) body.emailVerified = false;
|
|
531
|
+
next();
|
|
532
|
+
});
|
|
533
|
+
UserModel.remoteMethod("login", {
|
|
534
|
+
description: "Login a user with username/email and password.",
|
|
535
|
+
accepts: [{
|
|
536
|
+
arg: "credentials",
|
|
537
|
+
type: "object",
|
|
538
|
+
required: true,
|
|
539
|
+
http: { source: "body" }
|
|
540
|
+
}, {
|
|
541
|
+
arg: "include",
|
|
542
|
+
type: ["string"],
|
|
543
|
+
http: { source: "query" },
|
|
544
|
+
description: "Related objects to include in the response. See the description of return value for more details."
|
|
545
|
+
}],
|
|
546
|
+
returns: {
|
|
547
|
+
arg: "accessToken",
|
|
548
|
+
type: "object",
|
|
549
|
+
root: true,
|
|
550
|
+
description: g.f("The response body contains properties of the {{AccessToken}} created on login.\nDepending on the value of `include` parameter, the body may contain additional properties:\n\n - `user` - `U+007BUserU+007D` - Data of the currently logged in user. {{(`include=user`)}}\n\n")
|
|
551
|
+
},
|
|
552
|
+
http: { verb: "post" }
|
|
553
|
+
});
|
|
554
|
+
UserModel.remoteMethod("logout", {
|
|
555
|
+
description: "Logout a user with access token.",
|
|
556
|
+
accepts: [{
|
|
557
|
+
arg: "access_token",
|
|
558
|
+
type: "string",
|
|
559
|
+
http: function(ctx) {
|
|
560
|
+
const req = ctx && ctx.req;
|
|
561
|
+
const accessToken = req && req.accessToken;
|
|
562
|
+
return accessToken ? accessToken.id : void 0;
|
|
563
|
+
},
|
|
564
|
+
description: "Do not supply this argument, it is automatically extracted from request headers."
|
|
565
|
+
}],
|
|
566
|
+
http: { verb: "all" }
|
|
567
|
+
});
|
|
568
|
+
UserModel.remoteMethod("prototype.verify", {
|
|
569
|
+
description: "Trigger user's identity verification with configured verifyOptions",
|
|
570
|
+
accepts: [{
|
|
571
|
+
arg: "verifyOptions",
|
|
572
|
+
type: "object",
|
|
573
|
+
http: (ctx) => this.getVerifyOptions()
|
|
574
|
+
}, {
|
|
575
|
+
arg: "options",
|
|
576
|
+
type: "object",
|
|
577
|
+
http: "optionsFromRequest"
|
|
578
|
+
}],
|
|
579
|
+
http: { verb: "post" }
|
|
580
|
+
});
|
|
581
|
+
UserModel.remoteMethod("confirm", {
|
|
582
|
+
description: "Confirm a user registration with identity verification token.",
|
|
583
|
+
accepts: [
|
|
584
|
+
{
|
|
585
|
+
arg: "uid",
|
|
586
|
+
type: "string",
|
|
587
|
+
required: true
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
arg: "token",
|
|
591
|
+
type: "string",
|
|
592
|
+
required: true
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
arg: "redirect",
|
|
596
|
+
type: "string"
|
|
597
|
+
}
|
|
598
|
+
],
|
|
599
|
+
http: {
|
|
600
|
+
verb: "get",
|
|
601
|
+
path: "/confirm"
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
UserModel.remoteMethod("resetPassword", {
|
|
605
|
+
description: "Reset password for a user with email.",
|
|
606
|
+
accepts: [{
|
|
607
|
+
arg: "options",
|
|
608
|
+
type: "object",
|
|
609
|
+
required: true,
|
|
610
|
+
http: { source: "body" }
|
|
611
|
+
}],
|
|
612
|
+
http: {
|
|
613
|
+
verb: "post",
|
|
614
|
+
path: "/reset"
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
UserModel.remoteMethod("changePassword", {
|
|
618
|
+
description: "Change a user's password.",
|
|
619
|
+
accepts: [
|
|
620
|
+
{
|
|
621
|
+
arg: "id",
|
|
622
|
+
type: "any",
|
|
623
|
+
http: getUserIdFromRequestContext
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
arg: "oldPassword",
|
|
627
|
+
type: "string",
|
|
628
|
+
required: true,
|
|
629
|
+
http: { source: "form" }
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
arg: "newPassword",
|
|
633
|
+
type: "string",
|
|
634
|
+
required: true,
|
|
635
|
+
http: { source: "form" }
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
arg: "options",
|
|
639
|
+
type: "object",
|
|
640
|
+
http: "optionsFromRequest"
|
|
641
|
+
}
|
|
642
|
+
],
|
|
643
|
+
http: {
|
|
644
|
+
verb: "POST",
|
|
645
|
+
path: "/change-password"
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
const setPasswordScopes = UserModel.settings.restrictResetPasswordTokenScope ? ["reset-password"] : void 0;
|
|
649
|
+
UserModel.remoteMethod("setPassword", {
|
|
650
|
+
description: "Reset user's password via a password-reset token.",
|
|
651
|
+
accepts: [
|
|
652
|
+
{
|
|
653
|
+
arg: "id",
|
|
654
|
+
type: "any",
|
|
655
|
+
http: getUserIdFromRequestContext
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
arg: "newPassword",
|
|
659
|
+
type: "string",
|
|
660
|
+
required: true,
|
|
661
|
+
http: { source: "form" }
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
arg: "options",
|
|
665
|
+
type: "object",
|
|
666
|
+
http: "optionsFromRequest"
|
|
667
|
+
}
|
|
668
|
+
],
|
|
669
|
+
accessScopes: setPasswordScopes,
|
|
670
|
+
http: {
|
|
671
|
+
verb: "POST",
|
|
672
|
+
path: "/reset-password"
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
function getUserIdFromRequestContext(ctx) {
|
|
676
|
+
const token = ctx.req.accessToken;
|
|
677
|
+
if (!token) return;
|
|
678
|
+
if ("principalType" in token && token.principalType !== UserModel.modelName) {
|
|
679
|
+
const err = new Error(g.f("Access Denied"));
|
|
680
|
+
err.statusCode = 403;
|
|
681
|
+
throw err;
|
|
682
|
+
}
|
|
683
|
+
return token.userId;
|
|
684
|
+
}
|
|
685
|
+
UserModel.afterRemote("confirm", function(ctx, inst, next) {
|
|
686
|
+
if (ctx.args.redirect !== void 0) {
|
|
687
|
+
if (!ctx.res) return next(new Error(g.f("The transport does not support HTTP redirects.")));
|
|
688
|
+
ctx.res.location(ctx.args.redirect);
|
|
689
|
+
ctx.res.status(302);
|
|
690
|
+
}
|
|
691
|
+
next();
|
|
692
|
+
});
|
|
693
|
+
assert(loopback.Email, "Email model must be defined before User model");
|
|
694
|
+
UserModel.email = loopback.Email;
|
|
695
|
+
assert(loopback.AccessToken, "AccessToken model must be defined before User model");
|
|
696
|
+
UserModel.accessToken = loopback.AccessToken;
|
|
697
|
+
UserModel.validate("email", emailValidator, { message: g.f("Must provide a valid email") });
|
|
698
|
+
if (UserModel.settings.realmRequired && UserModel.settings.realmDelimiter) {
|
|
699
|
+
UserModel.validatesUniquenessOf("email", {
|
|
700
|
+
message: "Email already exists",
|
|
701
|
+
scopedTo: ["realm"]
|
|
702
|
+
});
|
|
703
|
+
UserModel.validatesUniquenessOf("username", {
|
|
704
|
+
message: "User already exists",
|
|
705
|
+
scopedTo: ["realm"]
|
|
706
|
+
});
|
|
707
|
+
} else {
|
|
708
|
+
UserModel.validatesUniquenessOf("email", { message: "Email already exists" });
|
|
709
|
+
UserModel.validatesUniquenessOf("username", { message: "User already exists" });
|
|
710
|
+
}
|
|
711
|
+
return UserModel;
|
|
712
|
+
};
|
|
713
|
+
User.setup();
|
|
714
|
+
User.observe("access", function normalizeEmailCase(ctx, next) {
|
|
715
|
+
if (!ctx.Model.settings.caseSensitiveEmail && ctx.query.where && ctx.query.where.email && typeof ctx.query.where.email === "string") ctx.query.where.email = ctx.query.where.email.toLowerCase();
|
|
716
|
+
next();
|
|
717
|
+
});
|
|
718
|
+
User.observe("before save", function rejectInsecurePasswordChange(ctx, next) {
|
|
719
|
+
if (!ctx.Model.settings.rejectPasswordChangesViaPatchOrReplace) return next();
|
|
720
|
+
if (ctx.isNewInstance) return next();
|
|
721
|
+
const data = ctx.data || ctx.instance;
|
|
722
|
+
const isPasswordChange = "password" in data;
|
|
723
|
+
if (ctx.options.setPassword) {
|
|
724
|
+
if (hasMoreThanOneOwnEnumerableKey(data) || !isPasswordChange) return next(/* @__PURE__ */ new Error("Invalid use of \"options.setPassword\". Only \"password\" can be changed when using this option."));
|
|
725
|
+
return next();
|
|
726
|
+
}
|
|
727
|
+
if (!isPasswordChange) return next();
|
|
728
|
+
const err = /* @__PURE__ */ new Error("Changing user password via patch/replace API is not allowed. Use changePassword() or setPassword() instead.");
|
|
729
|
+
err.statusCode = 401;
|
|
730
|
+
err.code = "PASSWORD_CHANGE_NOT_ALLOWED";
|
|
731
|
+
next(err);
|
|
732
|
+
});
|
|
733
|
+
User.observe("before save", function prepareForTokenInvalidation(ctx, next) {
|
|
734
|
+
if (ctx.isNewInstance) return next();
|
|
735
|
+
if (!ctx.where && !ctx.instance) return next();
|
|
736
|
+
const pkName = ctx.Model.definition.idName() || "id";
|
|
737
|
+
let where = ctx.where;
|
|
738
|
+
if (!where) {
|
|
739
|
+
where = {};
|
|
740
|
+
where[pkName] = ctx.instance[pkName];
|
|
741
|
+
}
|
|
742
|
+
ctx.Model.find({ where }, ctx.options, function(err, userInstances) {
|
|
743
|
+
if (err) return next(err);
|
|
744
|
+
const originalUserData = new Array(userInstances.length);
|
|
745
|
+
for (let i = 0; i < userInstances.length; i++) {
|
|
746
|
+
const u = userInstances[i];
|
|
747
|
+
const user = {};
|
|
748
|
+
user[pkName] = u[pkName];
|
|
749
|
+
user.email = u.email;
|
|
750
|
+
user.password = u.password;
|
|
751
|
+
originalUserData[i] = user;
|
|
752
|
+
}
|
|
753
|
+
ctx.hookState.originalUserData = originalUserData;
|
|
754
|
+
let emailChanged;
|
|
755
|
+
if (ctx.instance) {
|
|
756
|
+
if (ctx.hookState.originalUserData.length > 0) emailChanged = ctx.instance.email !== ctx.hookState.originalUserData[0].email;
|
|
757
|
+
else emailChanged = true;
|
|
758
|
+
if (emailChanged && ctx.Model.settings.emailVerificationRequired) ctx.instance.emailVerified = false;
|
|
759
|
+
} else if (ctx.data.email) {
|
|
760
|
+
emailChanged = ctx.hookState.originalUserData.some(function(data) {
|
|
761
|
+
return data.email != ctx.data.email;
|
|
762
|
+
});
|
|
763
|
+
if (emailChanged && ctx.Model.settings.emailVerificationRequired) ctx.data.emailVerified = false;
|
|
764
|
+
}
|
|
765
|
+
next();
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
User.observe("after save", function invalidateOtherTokens(ctx, next) {
|
|
769
|
+
if (!ctx.instance && !ctx.data) return next();
|
|
770
|
+
if (!ctx.hookState.originalUserData) return next();
|
|
771
|
+
const pkName = ctx.Model.definition.idName() || "id";
|
|
772
|
+
const newEmail = (ctx.instance || ctx.data).email;
|
|
773
|
+
const newPassword = (ctx.instance || ctx.data).password;
|
|
774
|
+
if (!newEmail && !newPassword) return next();
|
|
775
|
+
if (ctx.options.preserveAccessTokens) return next();
|
|
776
|
+
const userIdsToExpire = [];
|
|
777
|
+
for (let i = 0; i < ctx.hookState.originalUserData.length; i++) {
|
|
778
|
+
const u = ctx.hookState.originalUserData[i];
|
|
779
|
+
if (newEmail && u.email !== newEmail || newPassword && u.password !== newPassword) userIdsToExpire.push(u[pkName]);
|
|
780
|
+
}
|
|
781
|
+
ctx.Model._invalidateAccessTokensOfUsers(userIdsToExpire, ctx.options, next);
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
function hasMoreThanOneOwnEnumerableKey(object) {
|
|
785
|
+
let count = 0;
|
|
786
|
+
for (const key in object) if (hasOwnProperty.call(object, key)) {
|
|
787
|
+
count++;
|
|
788
|
+
if (count > 1) return true;
|
|
789
|
+
}
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
function emailValidator(err, done) {
|
|
793
|
+
const value = this.email;
|
|
794
|
+
if (value == null) return;
|
|
795
|
+
if (typeof value !== "string") return err("string");
|
|
796
|
+
if (value === "") return;
|
|
797
|
+
if (!isEmail.validate(value)) return err("email");
|
|
798
|
+
}
|
|
799
|
+
function joinUrlPath(args) {
|
|
800
|
+
let result = arguments[0];
|
|
801
|
+
for (let ix = 1; ix < arguments.length; ix++) {
|
|
802
|
+
const next = arguments[ix];
|
|
803
|
+
result += result[result.length - 1] === "/" && next[0] === "/" ? next.slice(1) : next;
|
|
804
|
+
}
|
|
805
|
+
return result;
|
|
806
|
+
}
|
|
807
|
+
module.exports = configureUser;
|
|
808
|
+
}));
|
|
809
|
+
//#endregion
|
|
810
|
+
module.exports = require_user();
|