Fix username unique in users-permissions plugin and implement sql db unique option

This commit is contained in:
Alexandre Bodin 2019-07-16 15:31:15 +02:00
parent 1658b48aa0
commit c4b7ae55e6
6 changed files with 209 additions and 204 deletions

View File

@ -108,25 +108,21 @@ module.exports = {
*/ */
async create(ctx) { async create(ctx) {
const values = ctx.request.body; const { email, username, password, blocked } = ctx.request.body;
if (!values.email) return ctx.badRequest('Missing email'); if (!email) return ctx.badRequest('missing.email');
if (!values.username) return ctx.badRequest('Missing username'); if (!username) return ctx.badRequest('missing.username');
if (!values.password) return ctx.badRequest('Missing password'); if (!password) return ctx.badRequest('missing.password');
const adminsWithSameEmail = await strapi const adminsWithSameEmail = await strapi
.query('administrator', 'admin') .query('administrator', 'admin')
.find({ .findOne({ email });
email: values.email,
});
const adminsWithSameUsername = await strapi const adminsWithSameUsername = await strapi
.query('administrator', 'admin') .query('administrator', 'admin')
.find({ .findOne({ username });
username: values.username,
});
if (adminsWithSameEmail.length > 0) { if (adminsWithSameEmail) {
return ctx.badRequest( return ctx.badRequest(
null, null,
ctx.request.admin ctx.request.admin
@ -137,11 +133,11 @@ module.exports = {
], ],
}, },
] ]
: 'Email is already taken.' : 'email.alreadyTaken'
); );
} }
if (adminsWithSameUsername.length > 0) { if (adminsWithSameUsername) {
return ctx.badRequest( return ctx.badRequest(
null, null,
ctx.request.admin ctx.request.admin
@ -155,15 +151,15 @@ module.exports = {
], ],
}, },
] ]
: 'Username is already taken.' : 'username.alreadyTaken.'
); );
} }
const user = { const user = {
email: values.email, email: email,
username: values.username, username: username,
blocked: values.blocked === true ? true : false, blocked: blocked === true ? true : false,
password: await strapi.admin.services.auth.hashPassword(values.password), password: await strapi.admin.services.auth.hashPassword(password),
}; };
const data = await strapi.query('administrator', 'admin').create(user); const data = await strapi.query('administrator', 'admin').create(user);
@ -180,11 +176,11 @@ module.exports = {
async update(ctx) { async update(ctx) {
const { id } = ctx.params; const { id } = ctx.params;
const values = ctx.request.body; const { email, username, password, blocked } = ctx.request.body;
if (!values.email) return ctx.badRequest('Missing email'); if (!email) return ctx.badRequest('Missing email');
if (!values.username) return ctx.badRequest('Missing username'); if (!username) return ctx.badRequest('Missing username');
if (!values.password) return ctx.badRequest('Missing password'); if (!password) return ctx.badRequest('Missing password');
const admin = await strapi const admin = await strapi
.query('administrator', 'admin') .query('administrator', 'admin')
@ -194,12 +190,10 @@ module.exports = {
if (!admin) return ctx.notFound('Administrator not found'); if (!admin) return ctx.notFound('Administrator not found');
// check there are not user with requested email // check there are not user with requested email
if (values.email !== admin.email) { if (email !== admin.email) {
const adminsWithSameEmail = await strapi const adminsWithSameEmail = await strapi
.query('administrator', 'admin') .query('administrator', 'admin')
.findOne({ .findOne({ email });
email: values.email,
});
if (adminsWithSameEmail && adminsWithSameEmail.id !== admin.id) { if (adminsWithSameEmail && adminsWithSameEmail.id !== admin.id) {
return ctx.badRequest( return ctx.badRequest(
@ -218,12 +212,10 @@ module.exports = {
} }
// check there are not user with requested username // check there are not user with requested username
if (values.username !== admin.username) { if (username !== admin.username) {
const adminsWithSameUsername = await strapi const adminsWithSameUsername = await strapi
.query('administrator', 'admin') .query('administrator', 'admin')
.findOne({ .findOne({ username });
username: values.username,
});
if (adminsWithSameUsername && adminsWithSameUsername.id !== admin.id) { if (adminsWithSameUsername && adminsWithSameUsername.id !== admin.id) {
return ctx.badRequest( return ctx.badRequest(
@ -245,15 +237,13 @@ module.exports = {
} }
const user = { const user = {
email: values.email, email: email,
username: values.username, username: username,
blocked: values.blocked === true ? true : false, blocked: blocked === true ? true : false,
}; };
if (values.password !== admin.password) { if (password !== admin.password) {
user.password = await strapi.admin.services.auth.hashPassword( user.password = await strapi.admin.services.auth.hashPassword(password);
values.password
);
} }
const data = await strapi const data = await strapi

View File

@ -16,36 +16,10 @@ module.exports = async ({
[createAtCol, updatedAtCol] = hasTimestamps; [createAtCol, updatedAtCol] = hasTimestamps;
} }
const quote = definition.client === 'pg' ? '"' : '`';
// Equilize database tables // Equilize database tables
const createOrUpdateTable = async (table, attributes) => { const createOrUpdateTable = async (table, attributes) => {
const tableExists = await ORM.knex.schema.hasTable(table); const tableExists = await ORM.knex.schema.hasTable(table);
// Apply field type of attributes definition
const generateColumns = (attrs, start, tableExists = false) => {
return Object.keys(attrs).reduce((acc, attr) => {
const attribute = attributes[attr];
const type = getType({
definition,
attribute,
name: attr,
tableExists,
});
if (type) {
acc.push(
`${quote}${attr}${quote} ${type} ${
attribute.required ? 'NOT NULL' : ''
}`
);
}
return acc;
}, start);
};
const generateIndexes = async table => { const generateIndexes = async table => {
try { try {
const connection = strapi.config.connections[definition.connection]; const connection = strapi.config.connections[definition.connection];
@ -104,40 +78,60 @@ module.exports = async ({
} }
}; };
const createTable = async table => { const buildColumns = (tbl, columns, opts = {}) => {
const defaultAttributeDefinitions = { const { tableExists, alter = false } = opts;
mysql: ['id INT AUTO_INCREMENT NOT NULL PRIMARY KEY'],
pg: ['id SERIAL NOT NULL PRIMARY KEY'],
sqlite3: ['id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'],
};
let idAttributeBuilder = defaultAttributeDefinitions[definition.client]; Object.keys(columns).forEach(key => {
const attribute = columns[key];
if (definition.primaryKeyType === 'uuid' && definition.client === 'pg') {
idAttributeBuilder = [
'id uuid NOT NULL DEFAULT uuid_generate_v4() NOT NULL PRIMARY KEY',
];
} else if (definition.primaryKeyType !== 'integer') {
const type = getType({ const type = getType({
definition, definition,
attribute: { attribute,
type: definition.primaryKeyType, name: key,
}, tableExists,
}); });
idAttributeBuilder = [`id ${type} NOT NULL PRIMARY KEY`]; if (type) {
} const col = tbl.specificType(key, type);
const columns = generateColumns(attributes, idAttributeBuilder).join(
',\n\r'
);
// Create table if (attribute.required === true) {
await ORM.knex.raw(`CREATE TABLE ${quote}${table}${quote} (${columns})`); if (definition.client !== 'sqlite3' || !tableExists) {
col.notNullable();
}
} else {
col.nullable();
}
if (attribute.unique === true) {
if (definition.client !== 'sqlite3' || !tableExists) {
tbl.unique(key, uniqueColName(table, key));
}
}
if (alter) {
col.alter();
}
}
});
};
const createColumns = (tbl, columns, opts = {}) => {
return buildColumns(tbl, columns, opts);
};
const alterColumns = (tbl, columns, opts = {}) => {
return createColumns(tbl, columns, { ...opts, alter: true });
};
const createTable = (table, { trx = ORM.knex, ...opts } = {}) => {
return trx.schema.createTable(table, tbl => {
tbl.specificType('id', getIdType(definition));
createColumns(tbl, attributes, { ...opts, tableExists: false });
});
}; };
if (!tableExists) { if (!tableExists) {
await createTable(table); await createTable(table);
await generateIndexes(table, attributes); await generateIndexes(table);
await storeTable(table, attributes); await storeTable(table, attributes);
return; return;
} }
@ -160,31 +154,16 @@ module.exports = async ({
} }
}); });
// Generate indexes for new attributes.
await generateIndexes(table, columnsToAdd);
// Generate and execute query to add missing column // Generate and execute query to add missing column
if (Object.keys(columnsToAdd).length > 0) { if (Object.keys(columnsToAdd).length > 0) {
await ORM.knex.schema.table(table, tbl => { await ORM.knex.schema.table(table, tbl => {
Object.keys(columnsToAdd).forEach(key => { createColumns(tbl, columnsToAdd, { tableExists });
const attribute = columnsToAdd[key];
const type = getType({
definition,
attribute,
name: key,
tableExists,
});
if (type) {
const col = tbl.specificType(key, type);
if (attribute.required && definition.client !== 'sqlite3') {
col.notNullable();
}
}
});
}); });
} }
// Generate indexes for new attributes.
await generateIndexes(table, columnsToAdd);
let previousAttributes; let previousAttributes;
try { try {
previousAttributes = JSON.parse( previousAttributes = JSON.parse(
@ -201,14 +180,26 @@ module.exports = async ({
); );
} }
if (JSON.stringify(previousAttributes) === JSON.stringify(attributes)) if (JSON.stringify(previousAttributes) === JSON.stringify(attributes)) {
return; return;
}
if (definition.client === 'sqlite3') { if (definition.client === 'sqlite3') {
const tmpTable = `tmp_${table}`; const tmpTable = `tmp_${table}`;
await createTable(tmpTable);
try { const rebuildTable = async trx => {
await trx.schema.renameTable(table, tmpTable);
// drop possible conflicting indexes
await Promise.all(
columns.map(key =>
trx.raw('DROP INDEX IF EXISTS ??', uniqueColName(table, key))
)
);
// create the table
await createTable(table, { trx });
const attrs = Object.keys(attributes).filter(attribute => const attrs = Object.keys(attributes).filter(attribute =>
getType({ getType({
definition, definition,
@ -217,51 +208,50 @@ module.exports = async ({
}) })
); );
await ORM.knex.raw(`INSERT INTO ?? (${attrs.join(', ')}) ??`, [ await trx.raw(`INSERT INTO ?? (${attrs.join(', ')}) ??`, [
tmpTable, table,
ORM.knex.select(attrs).from(table), trx.select(attrs).from(tmpTable),
]); ]);
await trx.schema.dropTableIfExists(tmpTable);
};
try {
await ORM.knex.transaction(trx => rebuildTable(trx));
await generateIndexes(table);
} catch (err) { } catch (err) {
strapi.log.error('Migration failed'); strapi.log.error('Migration failed');
strapi.log.error(err); strapi.log.error(err);
await ORM.knex.schema.dropTableIfExists(tmpTable);
return false; return false;
} }
await ORM.knex.schema.dropTableIfExists(table);
await ORM.knex.schema.renameTable(tmpTable, table);
await generateIndexes(table, attributes);
} else { } else {
await ORM.knex.schema.alterTable(table, tbl => { const columnsToAlter = columns.filter(
columns.forEach(key => { key =>
if ( JSON.stringify(previousAttributes[key]) !==
JSON.stringify(previousAttributes[key]) === JSON.stringify(attributes[key])
JSON.stringify(attributes[key]) );
)
return;
const attribute = attributes[key]; const alterTable = async trx => {
const type = getType({ await Promise.all(
definition, columnsToAlter.map(col => {
attribute, return ORM.knex.schema
name: key, .alterTable(table, tbl => {
tbl.dropUnique(col, uniqueColName(table, col));
})
.catch(() => {});
})
);
await trx.schema.alterTable(table, tbl => {
alterColumns(tbl, _.pick(attributes, columnsToAlter), {
tableExists, tableExists,
}); });
if (type) {
let col = tbl.specificType(key, type);
if (attribute.required) {
col = col.notNullable();
}
col.alter();
}
}); });
}); };
await storeTable(table, attributes); await ORM.knex.transaction(trx => alterTable(trx));
} }
await storeTable(table, attributes);
}; };
// Add created_at and updated_at field if timestamp option is true // Add created_at and updated_at field if timestamp option is true
@ -437,3 +427,30 @@ const storeTable = async (table, attributes) => {
value: JSON.stringify(attributes), value: JSON.stringify(attributes),
}).save(); }).save();
}; };
const defaultIdType = {
mysql: 'INT AUTO_INCREMENT NOT NULL PRIMARY KEY',
pg: 'SERIAL NOT NULL PRIMARY KEY',
sqlite3: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL',
};
const getIdType = definition => {
if (definition.primaryKeyType === 'uuid' && definition.client === 'pg') {
return 'uuid NOT NULL DEFAULT uuid_generate_v4() NOT NULL PRIMARY KEY';
}
if (definition.primaryKeyType !== 'integer') {
const type = getType({
definition,
attribute: {
type: definition.primaryKeyType,
},
});
return `${type} NOT NULL PRIMARY KEY`;
}
return defaultIdType[definition.client];
};
const uniqueColName = (table, key) => `${table}_${key}_unique`;

View File

@ -37,7 +37,9 @@ module.exports = async ({ model, definition, ORM, GLOBALS }) => {
}; };
}); });
await ORM.knex.schema.createTableIfNotExists(joinTable, table => { if (await ORM.knex.schema.hasTable(joinTable)) return;
await ORM.knex.schema.createTable(joinTable, table => {
table.increments(); table.increments();
table.string('field').notNullable(); table.string('field').notNullable();
table table
@ -45,14 +47,8 @@ module.exports = async ({ model, definition, ORM, GLOBALS }) => {
.unsigned() .unsigned()
.notNullable(); .notNullable();
table.string('slice_type').notNullable(); table.string('slice_type').notNullable();
table table.integer('slice_id').notNullable();
.integer('slice_id') table.integer(joinColumn).notNullable();
.unsigned()
.notNullable();
table
.integer(joinColumn)
.unsigned()
.notNullable();
table table
.foreign(joinColumn) .foreign(joinColumn)

View File

@ -114,8 +114,6 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
global[definition.globalName] = {}; global[definition.globalName] = {};
} }
await genGroupRelatons({ model: loadedModel, definition, ORM, GLOBALS });
// Add every relationships to the loaded model for Bookshelf. // Add every relationships to the loaded model for Bookshelf.
// Basic attributes don't need this-- only relations. // Basic attributes don't need this-- only relations.
Object.keys(definition.attributes).forEach(name => { Object.keys(definition.attributes).forEach(name => {
@ -709,13 +707,15 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
target[model]._attributes = definition.attributes; target[model]._attributes = definition.attributes;
target[model].updateRelations = relations.update; target[model].updateRelations = relations.update;
return buildDatabaseSchema({ await buildDatabaseSchema({
ORM, ORM,
definition, definition,
loadedModel, loadedModel,
connection, connection,
model: target[model], model: target[model],
}); });
await genGroupRelatons({ model: loadedModel, definition, ORM, GLOBALS });
} catch (err) { } catch (err) {
strapi.log.error(`Impossible to register the '${model}' model.`); strapi.log.error(`Impossible to register the '${model}' model.`);
strapi.log.error(err); strapi.log.error(err);

View File

@ -8,12 +8,10 @@
const _ = require('lodash'); const _ = require('lodash');
const sanitizeUser = user => { const sanitizeUser = user => _.omit(user, ['password', 'resetPasswordToken']);
return _.omit(user.toJSON ? user.toJSON() : user, [ const adminError = error => [
'password', { messages: [{ id: error.message, field: error.field }] },
'resetPasswordToken', ];
]);
};
module.exports = { module.exports = {
/** /**
@ -87,49 +85,72 @@ module.exports = {
}) })
.get(); .get();
if (advanced.unique_email && ctx.request.body.email) { const { email, username, password, role } = ctx.request.body;
if (!email) return ctx.badRequest('missing.email');
if (!username) return ctx.badRequest('missing.username');
if (!password) return ctx.badRequest('missing.password');
const adminsWithSameUsername = await strapi
.query('user', 'users-permissions')
.findOne({ username });
if (adminsWithSameUsername) {
return ctx.badRequest(
null,
ctx.request.admin
? adminError({
message: 'Auth.form.error.username.taken',
field: ['username'],
})
: 'username.alreadyTaken.'
);
}
if (advanced.unique_email) {
const user = await strapi const user = await strapi
.query('user', 'users-permissions') .query('user', 'users-permissions')
.findOne({ email: ctx.request.body.email }); .findOne({ email });
if (user) { if (user) {
return ctx.badRequest( return ctx.badRequest(
null, null,
ctx.request.admin ctx.request.admin
? [ ? adminError({
{ message: 'Auth.form.error.email.taken',
messages: [ field: ['email'],
{ id: 'Auth.form.error.email.taken', field: ['email'] }, })
], : 'email.alreadyTaken'
},
]
: 'Email is already taken.'
); );
} }
} }
if (!ctx.request.body.role) { const user = {
email,
username,
password,
role,
provider: 'local',
};
if (!role) {
const defaultRole = await strapi const defaultRole = await strapi
.query('role', 'users-permissions') .query('role', 'users-permissions')
.findOne({ type: advanced.default_role }, []); .findOne({ type: advanced.default_role }, []);
ctx.request.body.role = defaultRole._id || defaultRole.id; user.role = defaultRole.id;
} }
ctx.request.body.provider = 'local';
try { try {
const data = await strapi.plugins['users-permissions'].services.user.add( const data = await strapi.plugins['users-permissions'].services.user.add(
ctx.request.body user
); );
ctx.created(data); ctx.created(data);
} catch (error) { } catch (error) {
ctx.badRequest( ctx.badRequest(
null, null,
ctx.request.admin ctx.request.admin ? adminError(error) : error.message
? [{ messages: [{ id: error.message, field: error.field }] }]
: error.message
); );
} }
}, },
@ -166,13 +187,10 @@ module.exports = {
return ctx.badRequest( return ctx.badRequest(
null, null,
ctx.request.admin ctx.request.admin
? [ ? adminError({
{ message: 'Auth.form.error.email.taken',
messages: [ field: ['email'],
{ id: 'Auth.form.error.email.taken', field: ['email'] }, })
],
},
]
: 'Email is already taken.' : 'Email is already taken.'
); );
} }
@ -206,13 +224,10 @@ module.exports = {
return ctx.badRequest( return ctx.badRequest(
null, null,
ctx.request.admin ctx.request.admin
? [ ? adminError({
{ message: 'Auth.form.error.email.taken',
messages: [ field: ['email'],
{ id: 'Auth.form.error.email.taken', field: ['email'] }, })
],
},
]
: 'Email is already taken.' : 'Email is already taken.'
); );
} }

View File

@ -22,19 +22,6 @@ module.exports = {
].services.user.hashPassword(values); ].services.user.hashPassword(values);
} }
// Use Content Manager business logic to handle relation.
if (strapi.plugins['content-manager']) {
return await strapi.plugins['content-manager'].services[
'contentmanager'
].add(
{
model: 'user',
},
values,
'users-permissions'
);
}
return strapi.query('user', 'users-permissions').create(values); return strapi.query('user', 'users-permissions').create(values);
}, },