Archived
2

added /user DELETE route

This commit is contained in:
VLE2FE 2020-04-27 15:10:14 +02:00
parent 1eff39bb16
commit 5a911a455b
6 changed files with 239 additions and 73 deletions

View File

@ -43,7 +43,7 @@
500:
$ref: 'oas.yaml#/components/responses/500'
put:
summary: TODO change user details
summary: change user details
description: 'Auth: basic, levels: read, write, maintain, admin'
tags:
- /user
@ -85,7 +85,7 @@
500:
$ref: 'oas.yaml#/components/responses/500'
delete:
summary: TODO delete user
summary: delete user
description: 'Auth: basic, levels: read, write, maintain, admin'
tags:
- /user
@ -94,12 +94,8 @@
responses:
200:
$ref: 'oas.yaml#/components/responses/Ok'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
500:
$ref: 'oas.yaml#/components/responses/500'
/user/{name}:
@ -128,7 +124,7 @@
500:
$ref: 'oas.yaml#/components/responses/500'
put:
summary: TODO change user details
summary: change user details
description: 'Auth: basic, levels: admin'
tags:
- /user
@ -158,7 +154,7 @@
500:
$ref: 'oas.yaml#/components/responses/500'
delete:
summary: TODO delete user
summary: delete user
description: 'Auth: basic, levels: admin'
tags:
- /user
@ -167,8 +163,6 @@
responses:
200:
$ref: 'oas.yaml#/components/responses/Ok'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
@ -179,7 +173,7 @@
$ref: 'oas.yaml#/components/responses/500'
/user/key:
get:
summary: TODO get API key for the user
summary: get API key for the user
description: 'Auth: basic, levels: read, write, maintain, dev, admin'
tags:
- /user

172
package-lock.json generated
View File

@ -98,6 +98,15 @@
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-3.0.0.tgz",
"integrity": "sha512-nohgNyv+1ViVcubKBh0+XiNJ3dO8nYu///9aJ4cgSqv70gBL+94SNy/iC2NLzKPT2Zt/QavrOkBVbZRLZmw6NQ=="
},
"@types/body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==",
"requires": {
"@types/connect": "*",
"@types/node": "*"
}
},
"@types/bson": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.2.tgz",
@ -111,6 +120,39 @@
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
},
"@types/connect": {
"version": "3.4.33",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz",
"integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==",
"requires": {
"@types/node": "*"
}
},
"@types/express": {
"version": "4.17.6",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.6.tgz",
"integrity": "sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w==",
"requires": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "*",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"@types/express-serve-static-core": {
"version": "4.17.5",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.5.tgz",
"integrity": "sha512-578YH5Lt88AKoADy0b2jQGwJtrBxezXtVe/MBqWXKZpqx91SnC0pVkVCcxcytz3lWW+cHBYDi3Ysh0WXc+rAYw==",
"requires": {
"@types/node": "*",
"@types/range-parser": "*"
}
},
"@types/mime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz",
"integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw=="
},
"@types/mocha": {
"version": "5.2.7",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz",
@ -139,6 +181,25 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.1.6.tgz",
"integrity": "sha512-Jg1F+bmxcpENHP23sVKkNuU3uaxPnsBMW0cLjleiikFKomJQbsn0Cqk2yDvQArqzZN6ABfBkZ0To7pQ8sLdWDg=="
},
"@types/qs": {
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.1.tgz",
"integrity": "sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw=="
},
"@types/range-parser": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA=="
},
"@types/serve-static": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz",
"integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==",
"requires": {
"@types/express-serve-static-core": "*",
"@types/mime": "*"
}
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@ -745,9 +806,9 @@
}
},
"es-abstract": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0.tgz",
"integrity": "sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug==",
"version": "1.17.5",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz",
"integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==",
"dev": true,
"requires": {
"es-to-primitive": "^1.2.1",
@ -1299,12 +1360,12 @@
"dev": true
},
"log-symbols": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
"integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
"integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
"dev": true,
"requires": {
"chalk": "^2.0.1"
"chalk": "^2.4.2"
}
},
"lowercase-keys": {
@ -1393,9 +1454,9 @@
}
},
"mocha": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-7.0.0.tgz",
"integrity": "sha512-CirsOPbO3jU86YKjjMzFLcXIb5YiGLUrjrXFHoJ3e2z9vWiaZVCZQ2+gtRGMPWF+nFhN6AWwLM/juzAQ6KRkbA==",
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.2.tgz",
"integrity": "sha512-o96kdRKMKI3E8U0bjnfqW4QMk12MwZ4mhdBTf+B5a1q9+aq2HRnj+3ZdJu0B/ZhJeK78MgYuv6L8d/rA5AeBJA==",
"dev": true,
"requires": {
"ansi-colors": "3.2.3",
@ -1409,9 +1470,9 @@
"growl": "1.10.5",
"he": "1.2.0",
"js-yaml": "3.13.1",
"log-symbols": "2.2.0",
"log-symbols": "3.0.0",
"minimatch": "3.0.4",
"mkdirp": "0.5.1",
"mkdirp": "0.5.5",
"ms": "2.1.1",
"node-environment-flags": "1.0.6",
"object.assign": "4.1.0",
@ -1419,8 +1480,8 @@
"supports-color": "6.0.0",
"which": "1.3.1",
"wide-align": "1.1.3",
"yargs": "13.3.0",
"yargs-parser": "13.1.1",
"yargs": "13.3.2",
"yargs-parser": "13.1.2",
"yargs-unparser": "1.6.0"
},
"dependencies": {
@ -1469,21 +1530,6 @@
"path-is-absolute": "^1.0.0"
}
},
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
@ -1709,9 +1755,9 @@
"integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw=="
},
"p-limit": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz",
"integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"requires": {
"p-try": "^2.0.0"
@ -2132,24 +2178,46 @@
"strip-ansi": "^4.0.0"
}
},
"string.prototype.trimleft": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz",
"integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==",
"string.prototype.trimend": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
"integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==",
"dev": true,
"requires": {
"define-properties": "^1.1.3",
"function-bind": "^1.1.1"
"es-abstract": "^1.17.5"
}
},
"string.prototype.trimleft": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz",
"integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==",
"dev": true,
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.5",
"string.prototype.trimstart": "^1.0.0"
}
},
"string.prototype.trimright": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz",
"integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz",
"integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==",
"dev": true,
"requires": {
"define-properties": "^1.1.3",
"function-bind": "^1.1.1"
"es-abstract": "^1.17.5",
"string.prototype.trimend": "^1.0.0"
}
},
"string.prototype.trimstart": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz",
"integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==",
"dev": true,
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.5"
}
},
"string_decoder": {
@ -2582,9 +2650,9 @@
"dev": true
},
"yargs": {
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz",
"integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==",
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"dev": true,
"requires": {
"cliui": "^5.0.0",
@ -2596,7 +2664,7 @@
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.1"
"yargs-parser": "^13.1.2"
},
"dependencies": {
"ansi-regex": {
@ -2628,21 +2696,13 @@
}
},
"yargs-parser": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz",
"integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==",
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"dependencies": {
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true
}
}
},
"yargs-unparser": {

View File

@ -16,6 +16,7 @@
"@apidevtools/json-schema-ref-parser": "^8.0.0",
"@hapi/joi": "^17.1.1",
"@types/bcrypt": "^3.0.0",
"@types/express": "^4.17.6",
"@types/mocha": "^5.2.7",
"@types/mongoose": "^5.7.12",
"@types/node": "^13.1.6",
@ -35,7 +36,7 @@
"typescript": "^3.7.4"
},
"devDependencies": {
"mocha": "^7.0.0",
"mocha": "^7.1.2",
"should": "^13.2.3",
"supertest": "^4.0.2"
}

View File

@ -236,6 +236,16 @@ describe('/user', () => {
});
});
});
it('rejects a username which is in the special names', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/user/new',
auth: {basic: 'admin'},
httpStatus: 400, default: false,
req: {email: 'j.doe@bosch.com', name: 'passreset', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'},
res: {status: 'Username already taken'}
});
});
it('rejects invalid user details', done => {
TestHelper.request(server, done, {
method: 'put',
@ -304,6 +314,73 @@ describe('/user', () => {
});
});
describe('DELETE /user/{name}', () => {
it('deletes own user details', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/user',
auth: {basic: 'janedoe'},
httpStatus: 200
}).end((err, res) => {
if (err) return done (err);
should(res.body).be.eql({status: 'OK'});
UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => {
if (err) return done(err);
should(data).have.lengthOf(0);
done();
});
});
});
it('deletes other user details for admin', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/user/janedoe',
auth: {basic: 'admin'},
httpStatus: 200
}).end((err, res) => {
if (err) return done (err);
should(res.body).be.eql({status: 'OK'});
UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => {
if (err) return done(err);
should(data).have.lengthOf(0);
done();
});
});
});
it('rejects requests from non-admins for another user', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/user/admin',
auth: {basic: 'janedoe'},
httpStatus: 403
});
});
it('rejects requests from a user API key', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/user',
auth: {key: 'janedoe'},
httpStatus: 401
});
});
it('rejects requests from an admin API key', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/user/janedoe',
auth: {key: 'admin'},
httpStatus: 401
});
});
it('returns 404 for an unknown user', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/user/unknown',
auth: {basic: 'admin'},
httpStatus: 404
});
});
});
describe('POST /user/new', () => {
it('returns the added user data', done => {
TestHelper.request(server, done, {
@ -365,6 +442,16 @@ describe('/user', () => {
});
});
});
it('rejects a username which is in the special names', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/user/new',
auth: {basic: 'admin'},
httpStatus: 400, default: false,
req: {email: 'j.doe@bosch.com', name: 'passreset', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'},
res: {status: 'Username already taken'}
});
});
it('rejects invalid user details', done => {
TestHelper.request(server, done, {
method: 'post',

View File

@ -36,7 +36,7 @@ router.get('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi
});
router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new
console.log(req.authDetails);
req.params.username = req.params[0];
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return;
let username = req.authDetails.username;
if (req.params.username !== undefined) {
@ -44,8 +44,6 @@ router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi
username = req.params.username;
}
const {error, value: user} = UserValidate.input(req.body, 'change' + (req.authDetails.level === 'admin'? 'admin' : ''));
console.log(error);
console.log(user);
if(error !== undefined) {
res.status(400).json({status: 'Invalid body format'});
return;
@ -59,7 +57,7 @@ router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi
if (user.hasOwnProperty('name') && user.name !== username) {
UserModel.find({name: user.name}).lean().exec( (err, data:any) => {
if (err) next(err);
if (data.length > 0) {
if (data.length > 0 || UserValidate.isSpecialName(user.name)) {
res.status(400).json({status: 'Username already taken'});
return;
}
@ -88,6 +86,26 @@ router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi
}
});
router.delete('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new. See https://forbeslindesay.github.io/express-route-tester/ for the generated regex
req.params.username = req.params[0];
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return;
let username = req.authDetails.username;
if (req.params.username !== undefined) {
if (!req.auth(res, ['admin'], 'basic')) return;
username = req.params.username;
}
UserModel.findOneAndDelete({name: username}).lean().exec( (err, data:any) => {
if (err) next(err);
if (data) {
res.json({status: 'OK'})
}
else {
res.status(404).json({status: 'Not found'});
}
});
});
router.get('/user/key', (req, res, next) => {
console.log('hmm');
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return;
@ -111,7 +129,7 @@ router.post('/user/new', (req, res, next) => {
// check that user does not already exist
UserModel.find({name: user.name}).lean().exec( (err, data:any) => {
if (err) next(err);
if (data.length > 0) {
if (data.length > 0 || UserValidate.isSpecialName(user.name)) {
res.status(400).json({status: 'Username already taken'});
return;
}

View File

@ -25,6 +25,8 @@ export default class UserValidate { // validate input for user
.allow('')
};
private static specialUsernames = ['admin', 'user', 'key', 'new', 'passreset']; // names a user cannot take
static input (data, param) {
if (param === 'new') {
return joi.object({
@ -71,4 +73,8 @@ export default class UserValidate { // validate input for user
}).validate(data, {stripUnknown: true})
return error !== undefined? null : value;
}
static isSpecialName (name) { // true if name belongs to special names
return this.specialUsernames.indexOf(name) > -1;
}
}