Skip to content

Commit

Permalink
feat(user-profile): create user profiles
Browse files Browse the repository at this point in the history
- get user profile details
- update user profile details
- create profile model
- add relationship between users and profiles tables
- add validation for profile inputs
- add tests for the endpoints
- document feature using swagger

[Delivers #166789996]
  • Loading branch information
IsaiahRn authored and dmithamo committed Jul 23, 2019
1 parent dcae0a5 commit 2cf9f56
Show file tree
Hide file tree
Showing 14 changed files with 510 additions and 19 deletions.
4 changes: 2 additions & 2 deletions controllers/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ sgMail.setApiKey(process.env.SENDGRID_API_KEY);

export const signup = async (req, res) => {
const {
username, firstname, lastname, email, password,
username, firstname, lastname, email, password
} = req.body;
const hashedPassword = Auth.hashPassword(password);
const transaction = await sequelize.transaction();
const transaction = await sequelize.transaction({ autocommit: false });
try {
const newUser = await users.create({
username,
Expand Down
120 changes: 120 additions & 0 deletions controllers/profile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import db from '../models';

const { users } = db;

/**
* @exports Profile
* @class Profile
* @description Handles User Profile
* */
class Profile {
/**
* Get all user profiles by username
* @async
* @param {object} req - Request object
* @param {object} res - Response object
* @return {json} Returns json object
* @static
*/
static async getProfile(req, res) {
try {
const { username } = req.params;
const user = await users.findOne({
where: { username }
});

if (!user) {
return res.status(404).json({
error: 'User does not exists!',
});
}

const {
firstName, lastName, bio, image, phone, facebook, twitter, linkedIn, instagram, location
} = user;

return res.status(200).json({
message: 'User profile retrieved!',
profile: {
firstName,
lastName,
bio,
image,
phone,
facebook,
twitter,
linkedIn,
instagram,
location
},
});
} catch (err) {
return res.status(500).json({
error: 'Failed to retrieve user profile'
});
}
}

/**
* Update user profile
* @async
* @param {object} req - Request object
* @param {object} res - Response object
* @return {json} Returns json object
* @static
*/
static async updateProfile(req, res) {
try {
const { email } = req.decoded;
const {
firstName, lastName, bio, phone, facebook, twitter, linkedIn, instagram, location
} = req.body;

const user = await users.findOne({
where: { email }
});

if (!user) {
return res.status(404).json({
error: 'User does not exists!',
});
}

await user.update({
firstName,
lastName,
bio,
image: (req.file ? req.file.url : user.image),
phone,
facebook,
twitter,
linkedIn,
instagram,
location
});


return res.status(200).json({
message: 'User profile updated!',
profile: {
firstName,
lastName,
bio,
image: user.image,
phone,
facebook,
twitter,
linkedIn,
instagram,
location,
},
});
} catch (error) {
return res.status(500).json({
error: 'Failed to update user profile'
});
}
}
}

export default Profile;
3 changes: 1 addition & 2 deletions middlewares/imageUpload.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ cloudinary.config({
});
const storage = cloudinaryStorage({
cloudinary,
folder: 'articles',
folder: 'uploads',
allowedFormats: ['jpg', 'png'],
transformation: [{ width: 500, height: 500, crop: 'limit' }]
});

const configuration = multer({ storage }).single('file');
Expand Down
34 changes: 32 additions & 2 deletions middlewares/validations/authValidations.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ class authValidations {
case pwdRegex.test(password) === false:
return res.status(400).json({
error: [
'a valid password should not be alphanumeric',
'a valid password should have atleast a digit, a special character and an uppercase letter',
'a valid password should not be alphanumeric',
'a valid password should be 8 characters long',
'an example of a valid password is alphamugerwa'
'an example of a valid password is Explorer@47'
]
});
}
Expand All @@ -58,9 +60,11 @@ class authValidations {
case pwdRegex.test(password) === false:
return res.status(400).json({
error: [
'a valid password should not be alphanumeric',
'a valid password should have atleast a digit, a special character and an uppercase letter',
'a valid password should not be alphanumeric',
'a valid password should be 8 characters long',
'an example of a valid password is alphamugerwa'
'an example of a valid password is Explorer@47'
]
});

Expand All @@ -73,6 +77,32 @@ class authValidations {
next();
}

static async validateProfile(req, res, next) {
const {
firstName, lastName, phone
} = req.body;
const nameRegex = /^[a-zA-Z]*$/;
const phoneRegex = /^\+[0-9]?()[0-9](\s|\S)(\d[0-9]{9})$/gm;

switch (true) {
case nameRegex.test(firstName) === false:
return res.status(400).json({
error: 'Firstname should be alphabetic only'
});

case nameRegex.test(lastName) === false:
return res.status(400).json({
error: 'Lastname should be alphabetic only'
});
case phoneRegex.test(phone) === false:
return res.status(400).json({
error: 'Provide a valid phone number, i.e:+2507213315000'
});
}

next();
}

static async validatePasswordOnReset(req, res, next) {
const pwdRegex = /^(?=.*?[A-Z])(?=(.*[a-z]){1,})(?=(.*[\d]){1,})(?=(.*[\W]){1,})(?!.*\s).{8,}$/;
if (!pwdRegex.test(req.body.password)) {
Expand Down
38 changes: 35 additions & 3 deletions migrations/20190704173059-createTableUsers.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ export const up = (queryInterface, Sequelize) => queryInterface.createTable('use
allowNull: false,
primaryKey: true
},
firstName: { type: Sequelize.STRING },
lastName: { type: Sequelize.STRING },
firstName: { type: Sequelize.STRING, allowNull: true },
lastName: { type: Sequelize.STRING, allowNull: true },
username: {
type: Sequelize.STRING,
allowNull: false,
Expand All @@ -27,6 +27,38 @@ export const up = (queryInterface, Sequelize) => queryInterface.createTable('use
allowNull: false,
required: true
},
bio: {
type: Sequelize.STRING,
allowNull: true
},
image: {
type: Sequelize.STRING,
allowNull: true
},
phone: {
type: Sequelize.STRING,
allowNull: true
},
facebook: {
type: Sequelize.STRING,
allowNull: true
},
twitter: {
type: Sequelize.STRING,
allowNull: true
},
linkedIn: {
type: Sequelize.STRING,
allowNull: true
},
instagram: {
type: Sequelize.STRING,
allowNull: true
},
location: {
type: Sequelize.STRING,
allowNull: true
},
accessLevel: {
type: Sequelize.INTEGER,
defaultValue: '0'
Expand All @@ -45,7 +77,7 @@ export const up = (queryInterface, Sequelize) => queryInterface.createTable('use
allowNull: false,
type: Sequelize.DATE,
defaultValue: Sequelize.fn('now')
},
}
});

export const down = queryInterface => queryInterface.dropTable('users');
36 changes: 34 additions & 2 deletions models/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ export default (sequelize, DataTypes) => {
allowNull: false,
primaryKey: true
},
firstName: { type: DataTypes.STRING },
lastName: { type: DataTypes.STRING },
firstName: { type: DataTypes.STRING, allowNull: true },
lastName: { type: DataTypes.STRING, allowNull: true },
username: {
type: DataTypes.STRING,
allowNull: false,
Expand All @@ -28,6 +28,38 @@ export default (sequelize, DataTypes) => {
allowNull: false,
required: true
},
bio: {
type: DataTypes.STRING,
allowNull: true
},
image: {
type: DataTypes.STRING,
allowNull: true
},
phone: {
type: DataTypes.STRING,
allowNull: true
},
facebook: {
type: DataTypes.STRING,
allowNull: true
},
twitter: {
type: DataTypes.STRING,
allowNull: true
},
linkedIn: {
type: DataTypes.STRING,
allowNull: true
},
instagram: {
type: DataTypes.STRING,
allowNull: true
},
location: {
type: DataTypes.STRING,
allowNull: true
},
accessLevel: {
type: DataTypes.INTEGER,
defaultValue: '0'
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"scripts": {
"start": "babel-node index.js && redis-server",
"dev": "NODE_ENV=development nodemon ./index.js --exec babel-node",
"test": "NODE_ENV=test npm run setup:test && mocha --require @babel/register ./tests --timeout 10000 --exit",
"test": "NODE_ENV=test npm run setup:test && NODE_ENV=test mocha --require @babel/register ./tests --timeout 10000 --exit",
"setup:test": "NODE_ENV=test npm run migrate:undo && NODE_ENV=test npm run migrate && NODE_ENV=test npm run seed",
"setup:dev": "NODE_ENV=development npm run migrate:undo && NODE_ENV=development npm run migrate && NODE_ENV=development npm run seed",
"coveralls": "nyc npm test && nyc report --reporter=text-lcov | coveralls && nyc --reporter=lcov --reporter=text-lcov npm test",
Expand Down Expand Up @@ -91,9 +91,11 @@
},
"nyc": {
"exclude": [
"test/*",
"tests/*",
"models/*",
"helpers/",
"migrations/*",
"middlewares/checkDb.js",
"seeders/*",
"config/*",
"index.js"
Expand Down
Loading

0 comments on commit 2cf9f56

Please sign in to comment.