profile: store preferred language in the database
This commit is contained in:
@@ -8,6 +8,7 @@ exports = module.exports = {
|
||||
setFallbackEmail,
|
||||
getAvatar,
|
||||
setAvatar,
|
||||
setLanguage,
|
||||
getBackgroundImage,
|
||||
setBackgroundImage,
|
||||
setPassword,
|
||||
@@ -65,6 +66,7 @@ async function get(req, res, next) {
|
||||
role: req.user.role,
|
||||
source: req.user.source,
|
||||
hasBackgroundImage: !!backgroundImage,
|
||||
language: req.user.language,
|
||||
avatarUrl: `https://${dashboardFqdn}/api/v1/profile/avatar/${req.user.id}`,
|
||||
avatarType: avatarType.toString() // this is a Buffer
|
||||
}));
|
||||
@@ -106,6 +108,18 @@ async function setDisplayName(req, res, next) {
|
||||
next(new HttpSuccess(204));
|
||||
}
|
||||
|
||||
async function setLanguage(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if ('language' in req.body && typeof req.body.language !== 'string') return next(new HttpError(400, 'language must be string'));
|
||||
|
||||
const [error] = await safe(users.update(req.user, { language: req.body.language }, AuditSource.fromRequest(req)));
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
}
|
||||
|
||||
async function setAvatar(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ describe('Profile API', function () {
|
||||
expect(response.body.displayName).to.be.a('string');
|
||||
expect(response.body.password).to.not.be.ok();
|
||||
expect(response.body.salt).to.not.be.ok();
|
||||
expect(response.body.language).to.be('');
|
||||
});
|
||||
|
||||
it('fails with expired token', async function () {
|
||||
@@ -370,4 +371,54 @@ describe('Profile API', function () {
|
||||
expect(response.body).to.eql(defaultAvatar);
|
||||
});
|
||||
});
|
||||
|
||||
describe('language', function () {
|
||||
it('fails to set unknown language', async function () {
|
||||
const response = await superagent.post(`${serverUrl}/api/v1/profile/language`)
|
||||
.query({ access_token: user.token })
|
||||
.send({ language: 'ta' })
|
||||
.ok(() => true);
|
||||
expect(response.statusCode).to.be(400);
|
||||
});
|
||||
|
||||
it('fails to set bad language', async function () {
|
||||
const response = await superagent.post(`${serverUrl}/api/v1/profile/language`)
|
||||
.query({ access_token: user.token })
|
||||
.send({ language: 123 })
|
||||
.ok(() => true);
|
||||
expect(response.statusCode).to.be(400);
|
||||
});
|
||||
|
||||
it('fails to set unknown language', async function () {
|
||||
const response = await superagent.post(`${serverUrl}/api/v1/profile/language`)
|
||||
.query({ access_token: user.token })
|
||||
.send({ language: 'ta' })
|
||||
.ok(() => true);
|
||||
expect(response.statusCode).to.be(400);
|
||||
});
|
||||
|
||||
it('set valid language', async function () {
|
||||
const response = await superagent.post(`${serverUrl}/api/v1/profile/language`)
|
||||
.query({ access_token: user.token })
|
||||
.send({ language: 'en' });
|
||||
expect(response.statusCode).to.be(204);
|
||||
});
|
||||
|
||||
it('did set language', async function () {
|
||||
const response = await superagent.get(`${serverUrl}/api/v1/profile`).query({ access_token: user.token });
|
||||
expect(response.body.language).to.contain('en');
|
||||
});
|
||||
|
||||
it('reset valid language', async function () {
|
||||
const response = await superagent.post(`${serverUrl}/api/v1/profile/language`)
|
||||
.query({ access_token: user.token })
|
||||
.send({ language: '' });
|
||||
expect(response.statusCode).to.be(204);
|
||||
});
|
||||
|
||||
it('did reset language', async function () {
|
||||
const response = await superagent.get(`${serverUrl}/api/v1/profile`).query({ access_token: user.token });
|
||||
expect(response.body.language).to.contain('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -163,6 +163,7 @@ async function initializeExpressSync() {
|
||||
router.post('/api/v1/profile/display_name', json, token, authorizeUser, routes.profile.canEditProfile, routes.profile.setDisplayName);
|
||||
router.post('/api/v1/profile/email', json, token, authorizeUser, routes.profile.canEditProfile, routes.users.verifyPassword, routes.profile.setEmail);
|
||||
router.post('/api/v1/profile/fallback_email', json, token, authorizeUser, routes.profile.canEditProfile, routes.users.verifyPassword, routes.profile.setFallbackEmail);
|
||||
router.post('/api/v1/profile/language', json, token, authorizeUser, routes.profile.setLanguage);
|
||||
router.get ('/api/v1/profile/avatar/:identifier', routes.profile.getAvatar); // this is not scoped so it can used directly in img tag
|
||||
router.post('/api/v1/profile/avatar', json, token, authorizeUser, (req, res, next) => { return typeof req.body.avatar === 'string' ? next() : multipart(req, res, next); }, routes.profile.setAvatar); // avatar is not exposed in LDAP. so it's personal and not locked
|
||||
router.get ('/api/v1/profile/background_image', token, authorizeUser, routes.profile.getBackgroundImage);
|
||||
|
||||
@@ -553,6 +553,32 @@ describe('User', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('language', function () {
|
||||
before(createOwner);
|
||||
|
||||
it('default language is empty', async function () {
|
||||
const result = await users.get(admin.id);
|
||||
expect(result.language).to.be('');
|
||||
});
|
||||
|
||||
it('cannot set bad language', async function () {
|
||||
const [error] = await safe(users.update(admin, { language: 'ta '}, auditSource));
|
||||
expect(error.reason).to.be(BoxError.BAD_FIELD);
|
||||
});
|
||||
|
||||
it('can set language', async function () {
|
||||
await users.update(admin, { language: 'en' }, auditSource);
|
||||
const result = await users.get(admin.id);
|
||||
expect(result.language).to.be('en');
|
||||
});
|
||||
|
||||
it('can reset language', async function () {
|
||||
await users.update(admin, { language: '' }, auditSource);
|
||||
const result = await users.get(admin.id);
|
||||
expect(result.language).to.be('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('invite', function () {
|
||||
before(createOwner);
|
||||
|
||||
|
||||
+2
-6
@@ -66,17 +66,13 @@ async function getTranslations() {
|
||||
}
|
||||
|
||||
async function listLanguages() {
|
||||
// we always return english to avoid dashboard breakage
|
||||
let languages = ['en'];
|
||||
|
||||
const [error, result] = await safe(fs.promises.readdir(TRANSLATION_FOLDER));
|
||||
if (error) {
|
||||
debug('listLanguages: Failed to list translations. %o', error);
|
||||
return languages;
|
||||
return [ 'en' ]; // we always return english to avoid dashboard breakage
|
||||
}
|
||||
|
||||
const jsonFiles = result.filter(function (file) { return path.extname(file) === '.json'; });
|
||||
languages = jsonFiles.map(function (file) { return path.basename(file, '.json'); });
|
||||
|
||||
const languages = jsonFiles.map(function (file) { return path.basename(file, '.json'); });
|
||||
return languages;
|
||||
}
|
||||
|
||||
+24
-6
@@ -72,7 +72,7 @@ exports = module.exports = {
|
||||
const ORDERED_ROLES = [ exports.ROLE_USER, exports.ROLE_USER_MANAGER, exports.ROLE_MAIL_MANAGER, exports.ROLE_ADMIN, exports.ROLE_OWNER ];
|
||||
|
||||
// the avatar and backgroundImage fields are special and not added here to reduce response sizes
|
||||
const USERS_FIELDS = [ 'id', 'username', 'email', 'fallbackEmail', 'password', 'salt', 'creationTime', 'inviteToken', 'resetToken', 'displayName',
|
||||
const USERS_FIELDS = [ 'id', 'username', 'email', 'fallbackEmail', 'password', 'salt', 'creationTime', 'inviteToken', 'resetToken', 'displayName', 'language',
|
||||
'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role', 'resetTokenCreationTime', 'loginLocationsJson' ].join(',');
|
||||
|
||||
const DEFAULT_GHOST_LIFETIME = 6 * 60 * 60 * 1000; // 6 hours
|
||||
@@ -96,6 +96,7 @@ const appPasswords = require('./apppasswords.js'),
|
||||
settings = require('./settings.js'),
|
||||
speakeasy = require('speakeasy'),
|
||||
tokens = require('./tokens.js'),
|
||||
translation = require('./translation.js'),
|
||||
uuid = require('uuid'),
|
||||
uaParser = require('ua-parser-js'),
|
||||
superagent = require('superagent'),
|
||||
@@ -164,6 +165,17 @@ function validateDisplayName(name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function validateLanguage(language) {
|
||||
assert.strictEqual(typeof language, 'string');
|
||||
|
||||
if (language === '') return null; // reset to platform default
|
||||
|
||||
const languages = await translation.listLanguages();
|
||||
if (!languages.includes(language)) return new BoxError(BoxError.BAD_FIELD, 'Invalid language');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validatePassword(password) {
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
|
||||
@@ -249,11 +261,12 @@ async function add(email, data, auditSource) {
|
||||
displayName: displayName,
|
||||
source: source,
|
||||
role: role,
|
||||
avatar: constants.AVATAR_NONE
|
||||
avatar: constants.AVATAR_NONE,
|
||||
language: ''
|
||||
};
|
||||
|
||||
const query = 'INSERT INTO users (id, username, password, email, fallbackEmail, salt, resetToken, inviteToken, displayName, source, role, avatar) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
const args = [ user.id, user.username, user.password, user.email, user.fallbackEmail, user.salt, user.resetToken, user.inviteToken, user.displayName, user.source, user.role, user.avatar ];
|
||||
const query = 'INSERT INTO users (id, username, password, email, fallbackEmail, salt, resetToken, inviteToken, displayName, source, role, avatar, language) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
const args = [ user.id, user.username, user.password, user.email, user.fallbackEmail, user.salt, user.resetToken, user.inviteToken, user.displayName, user.source, user.role, user.avatar, user.language ];
|
||||
|
||||
[error] = await safe(database.query(query, args));
|
||||
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('users_email') !== -1) throw new BoxError(BoxError.ALREADY_EXISTS, 'email already exists');
|
||||
@@ -608,8 +621,13 @@ async function update(user, data, auditSource) {
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
let args = [ ];
|
||||
let fields = [ ];
|
||||
if (data.language) {
|
||||
error = await validateLanguage(data.language);
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
let args = [];
|
||||
let fields = [];
|
||||
for (const k in data) {
|
||||
if (k === 'twoFactorAuthenticationEnabled' || k === 'active') {
|
||||
fields.push(k + ' = ?');
|
||||
|
||||
Reference in New Issue
Block a user