Skip to content

Commit 8edc2cf

Browse files
authored
feat: action to update the username (#616)
1 parent 6a23442 commit 8edc2cf

15 files changed

Lines changed: 983 additions & 437 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ lib
3838

3939
# documentation
4040
docs
41+
doc
4142
ss
4243

4344
# Other files

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"consul": "^0.40.0"
3030
},
3131
"dependencies": {
32-
"@faker-js/faker": "^9.0.3",
32+
"@faker-js/faker": "^9.2.0",
3333
"@fastify/deepmerge": "^2.0.0",
3434
"@hapi/bell": "^13.0.2",
3535
"@hapi/boom": "^10.0.1",
@@ -74,7 +74,7 @@
7474
"handlebars": "^4.7.8",
7575
"ioredis": "^4.28.5",
7676
"is": "^3.3.0",
77-
"jose": "^5.9.4",
77+
"jose": "^5.9.6",
7878
"jsonwebtoken": "^9.0.2",
7979
"jwa": "^2.0.0",
8080
"jwks-rsa": "^3.1.0",

pnpm-lock.yaml

Lines changed: 406 additions & 427 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

schemas/common.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,14 +176,19 @@
176176
"type": "string",
177177
"oneOf": [
178178
{ "format": "email" },
179-
{ "pattern": "^\\d+$" }
179+
{ "$ref": "#/definitions/phone" }
180180
]
181181
},
182182
"type": {
183183
"type": "string",
184184
"enum": ["email", "phone"]
185185
}
186186
}
187+
},
188+
"phone": {
189+
"description": "E-164. Only digits",
190+
"type": "string",
191+
"pattern": "^[0-9]{7,15}$"
187192
}
188193
}
189194
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"$id": "update-username.request",
3+
"type": "object",
4+
"required": [
5+
"challengeType",
6+
"username",
7+
"value"
8+
],
9+
"properties": {
10+
"challengeType": {
11+
"description": "Where to send the secret code. Currently only the `phone` is supported",
12+
"type": "string",
13+
"oneOf": [
14+
{
15+
"const": "phone"
16+
}
17+
]
18+
},
19+
"i18nLocale": {
20+
"description": "User locale",
21+
"maxLength": 10,
22+
"minLength": 1,
23+
"type": "string"
24+
},
25+
"username": {
26+
"description": "User alias, ID or username",
27+
"maxLength": 50,
28+
"minLength": 3,
29+
"type": "string"
30+
},
31+
"value": {
32+
"description": "New username",
33+
"type": "string",
34+
"oneOf": [
35+
{
36+
"$ref": "../common.json#/definitions/phone"
37+
}
38+
]
39+
}
40+
},
41+
"allOf": [
42+
{
43+
"if": {
44+
"properties": {
45+
"challengeType": {
46+
"const": "phone"
47+
}
48+
}
49+
},
50+
"then": {
51+
"properties": {
52+
"value": {
53+
"$ref": "../common.json#/definitions/phone"
54+
}
55+
}
56+
}
57+
}
58+
]
59+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"$id": "update-username.update",
3+
"type": "object",
4+
"required": [
5+
"token",
6+
"username"
7+
],
8+
"properties": {
9+
"token": {
10+
"description": "The code that was sent to the user using `update-username.request` action",
11+
"maxLength": 1000,
12+
"minLength": 3,
13+
"type": "string"
14+
},
15+
"username": {
16+
"description": "New username. Must match the username used in the username update request",
17+
"type": "string",
18+
"oneOf": [
19+
{
20+
"$ref": "../common.json#/definitions/phone"
21+
}
22+
]
23+
}
24+
}
25+
}

scripts/updateUsername.lua

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
local userDataKey = KEYS[1];
2+
local userMetadaKey = KEYS[2];
3+
local usernameToIdKey = KEYS[3];
4+
5+
local userId = ARGV[1];
6+
local newUsername = ARGV[2];
7+
local usernameField = ARGV[3];
8+
local newUsernameEncoded = ARGV[4];
9+
10+
if redis.call('hget', usernameToIdKey, newUsername) ~= false then
11+
return redis.error_reply('E_USERNAME_CONFLICT');
12+
end
13+
14+
local currentUsername = redis.call('hget', userDataKey, usernameField)
15+
16+
if currentUsername == false then
17+
return redis.error_reply('E_USER_ID_NOT_FOUND');
18+
end
19+
20+
redis.call('hdel', usernameToIdKey, currentUsername)
21+
redis.call('hset', usernameToIdKey, newUsername, userId)
22+
redis.call('hset', userDataKey, usernameField, newUsername)
23+
redis.call('hset', userMetadaKey, usernameField, newUsernameEncoded)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
const { ActionTransport } = require('@microfleet/plugin-router');
2+
3+
const { requestUsernameUpdate } = require('../../utils/update-username');
4+
const { resolveUserId } = require('../../utils/userData');
5+
const { checkMFA } = require('../../utils/mfa');
6+
const isActive = require('../../utils/is-active');
7+
const isBanned = require('../../utils/is-banned');
8+
const {
9+
ErrorConflictUserExists,
10+
ErrorUserNotFound,
11+
MFA_TYPE_OPTIONAL,
12+
USERS_ID_FIELD,
13+
} = require('../../constants');
14+
15+
/**
16+
* @api {amqp} <prefix>.update-username.request Request update username
17+
* @apiVersion 1.0.0
18+
* @apiName RequestUpdateUsername
19+
* @apiGroup Users
20+
*
21+
* @apiDescription Sends the user a secret code that will be used
22+
* to confirm the user name update. Currently only phone is supported.
23+
*
24+
* @apiSchema (Payload) {jsonschema=../../../schemas/update-username/request.json} apiParam
25+
*
26+
* @apiSuccess (Response) {Object} uid Token UID
27+
*/
28+
module.exports = async function requestUpdateUsernameAction(request) {
29+
const { challengeType, i18nLocale, value } = request.params;
30+
const { internalData } = request.locals;
31+
32+
if (!internalData) {
33+
throw ErrorUserNotFound;
34+
}
35+
36+
await isActive(internalData);
37+
isBanned(internalData);
38+
39+
if (await resolveUserId.call(this, value)) {
40+
throw ErrorConflictUserExists;
41+
}
42+
43+
return requestUsernameUpdate(
44+
this,
45+
internalData[USERS_ID_FIELD],
46+
value,
47+
challengeType,
48+
{ i18nLocale }
49+
);
50+
};
51+
52+
module.exports.mfa = MFA_TYPE_OPTIONAL;
53+
module.exports.allowed = checkMFA;
54+
module.exports.transports = [ActionTransport.amqp, ActionTransport.internal];
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const { ActionTransport } = require('@microfleet/plugin-router');
2+
3+
const { updateUsernameWithToken } = require('../../utils/update-username');
4+
5+
/**
6+
* @api {amqp} <prefix>.update-username Update Username
7+
* @apiVersion 1.0.0
8+
* @apiName UpdateUsername
9+
* @apiGroup Users
10+
*
11+
* @apiDescription Update username using the code that was sent
12+
* to the user using `update-username.request` action.
13+
* Currently only the phone is supported.
14+
*
15+
* @apiSchema (Payload) {jsonschema=../../../schemas/update-username/update.json} apiParam
16+
*/
17+
module.exports = async function updateUsernameAction(request) {
18+
const { token, username } = request.params;
19+
20+
await updateUsernameWithToken(this, token, username);
21+
};
22+
23+
module.exports.transports = [ActionTransport.amqp, ActionTransport.internal];

src/configs/verification-challenges.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const {
55
USERS_ACTION_DISPOSABLE_PASSWORD,
66
USERS_ACTION_REGISTER,
77
USERS_ACTION_RESET,
8+
USERS_ACTION_UPDATE_USERNAME,
89
USERS_ACTION_VERIFY_CONTACT,
910
} = require('../constants');
1011

@@ -59,10 +60,11 @@ exports.phone = {
5960
account: 'replace-with-your-account',
6061
messages: {
6162
[USERS_ACTION_ACTIVATE]: '%s is your activation code',
62-
[USERS_ACTION_VERIFY_CONTACT]: '%s is your verification code',
6363
[USERS_ACTION_DISPOSABLE_PASSWORD]: '%s is your disposable password',
6464
[USERS_ACTION_REGISTER]: '%s is your password',
6565
[USERS_ACTION_RESET]: '%s is your code for reset password',
66+
[USERS_ACTION_UPDATE_USERNAME]: '%s is your code for update username',
67+
[USERS_ACTION_VERIFY_CONTACT]: '%s is your verification code',
6668
},
6769
route: 'phone.message.predefined',
6870
publishOptions: {},

0 commit comments

Comments
 (0)