-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathactivate.js
More file actions
231 lines (201 loc) · 6.28 KB
/
activate.js
File metadata and controls
231 lines (201 loc) · 6.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
const Errors = require('common-errors');
const Promise = require('bluebird');
const redisKey = require('../utils/key.js');
const jwt = require('../utils/jwt.js');
const { getInternalData } = require('../utils/userData');
const getMetadata = require('../utils/get-metadata');
const handlePipeline = require('../utils/pipeline-error.js');
const InactiveUser = require('../utils/user/inactive-user');
const User = require('../utils/user/user');
const {
USERS_INDEX,
USERS_INACTIVATED,
USERS_DATA,
USERS_REFERRAL_INDEX,
USERS_PUBLIC_INDEX,
USERS_ACTIVE_FLAG,
USERS_ID_FIELD,
USERS_ALIAS_FIELD,
USERS_REFERRAL_FIELD,
USERS_USERNAME_FIELD,
USERS_ACTION_ACTIVATE,
} = require('../constants.js');
// cache error
const Forbidden = new Errors.HttpStatusError(403, 'invalid token');
const Inactive = new Errors.HttpStatusError(412, 'expired token, please request a new email');
const Active = new Errors.HttpStatusError(409, 'account is already active, please use sign in form');
const NotFound = new Errors.HttpStatusError(404, 'account not found');
/**
* Helper to determine if something is true
*/
function throwBasedOnStatus(status) {
if (status === 'true') {
throw Active;
}
throw Inactive;
}
/**
* Verifies that account is active
*/
function isAccountActive(username) {
return getInternalData
.call(this, username)
.then((userData) => userData[USERS_ACTIVE_FLAG])
.then(throwBasedOnStatus);
}
/**
* Modifies error from the token
*/
function RethrowForbidden(error) {
const { log, token, username } = this;
const { args, message } = error;
log.warn({ token, username, args }, 'failed to activate', message);
// remap error message
// and possibly status code
if (!args) {
throw Forbidden;
}
return Promise
.bind(this, args.id)
// if it's active will throw 409, otherwise 412
.then(isAccountActive);
}
function verifyToken(args, opts) {
const { tokenManager } = this;
return Promise
.resolve(tokenManager.verify(args, opts))
.bind(this)
.catch(RethrowForbidden)
.get('id');
}
function verifyRequest() {
const {
username, token, service: { tokenManager, redis, log }, erase,
} = this;
const action = USERS_ACTION_ACTIVATE;
const context = {
log, redis, token, username, tokenManager,
};
if (username && token) {
return getInternalData
.call(this.service, username)
.then((userData) => [
{ action, token, id: userData[USERS_USERNAME_FIELD] },
{ erase },
])
.bind(context)
.spread(verifyToken);
}
if (token) {
return verifyToken.call(context, token, { erase, control: { action } });
}
if (username) {
return Promise.resolve(username);
}
throw new Errors.HttpStatusError(400, 'invalid params');
}
/**
* Activates account after it was verified
* @param {Object} data internal user data
* @return {Promise}
*/
function activateAccount(data, metadata) {
const userId = data[USERS_ID_FIELD];
const alias = data[USERS_ALIAS_FIELD];
const referral = metadata[USERS_REFERRAL_FIELD];
const userKey = redisKey(userId, USERS_DATA);
// WARNING: `persist` is very important, otherwise we will lose user's information in 30 days
// set to active & persist
const pipeline = this.redis
.pipeline()
.hget(userKey, USERS_ACTIVE_FLAG)
.hset(userKey, USERS_ACTIVE_FLAG, 'true')
.persist(userKey)
.sadd(USERS_INDEX, userId);
/* delete user id from the inactive users index */
pipeline.zrem(USERS_INACTIVATED, userId);
if (alias) {
pipeline.sadd(USERS_PUBLIC_INDEX, userId);
}
if (referral) {
pipeline.sadd(`${USERS_REFERRAL_INDEX}:${referral}`, userId);
}
return pipeline
.exec()
.then(handlePipeline)
.spread((isActive) => {
if (isActive === 'true') {
throw new Errors.HttpStatusError(417, `Account ${userId} was already activated`);
}
})
.return(userId);
}
/**
* Invokes available hooks
*/
function hook(userId) {
return this.service.hook('users:activate', userId, { audience: this.audience });
}
async function checkUserDeleting(internalData) {
const user = new User(this);
if (await user.isUserDeleting(internalData.id)) {
throw NotFound;
}
}
/**
* @api {amqp} <prefix>.activate Activate User
* @apiVersion 1.0.0
* @apiName ActivateUser
* @apiGroup Users
*
* @apiDescription This method allows one to activate user by 3 means:
* 1) When only `username` is provided, no verifications will be performed and user will be set
* to active. This case is used when admin activates a user.
* 2) When only `token` is provided that means that token is encrypted and would be verified.
* This case is used when user completes verification challenge.
* 3) This case is similar to the previous, but used both `username` and `token` for
* verification. Use this when the token isn't decrypted.
* Success response contains user object.
*
* @apiParam (Payload) {String} username - id of the user
* @apiParam (Payload) {String} token - verification token
* @apiParam (Payload) {String} [remoteip] - not used, but is reserved for security log in the future
* @apiParam (Payload) {String} [audience] - additional metadata will be pushed there from custom hooks
*
*/
async function activateAction({ params }) {
// TODO: add security logs
// var remoteip = request.params.remoteip;
const { token, username } = params;
const { log, config } = this;
const audience = params.audience || config.defaultAudience;
log.debug('incoming request params %j', params);
// basic context
const context = {
audience,
token,
username,
service: this,
erase: config.token.erase,
};
const inactiveUsers = new InactiveUser(this);
await inactiveUsers.cleanUsersOnce(config.deleteInactiveAccounts);
return Promise
.bind(context)
.then(verifyRequest)
.bind(this)
.then((resolvedUsername) => getInternalData.call(this, resolvedUsername))
.tap(checkUserDeleting)
.then((internalData) => Promise.join(
internalData,
getMetadata.call(this, internalData[USERS_ID_FIELD], audience).get(audience)
))
.spread(activateAccount)
.bind(context)
.tap(hook)
.bind(this)
.then((userId) => [userId, audience])
.spread(jwt.login);
}
activateAction.transports = [require('@microfleet/core').ActionTransport.amqp];
module.exports = activateAction;