Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement profile picture #343 #558

Merged
merged 21 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6cfbb61
feat: implement profile picture #343
hubsMIT1 Oct 29, 2023
f64959c
feat: implement profile picture #343
hubsMIT1 Oct 29, 2023
0e5b44a
updated files #558
hubsMIT1 Oct 29, 2023
9c79b77
updated UserAvatar #558
hubsMIT1 Oct 29, 2023
2306e80
simplify isUnsafe with Math.max #558
hubsMIT1 Oct 29, 2023
5b91c12
Math.max returns Nan so updated
hubsMIT1 Oct 29, 2023
5b46774
imageUrl optional
hubsMIT1 Oct 29, 2023
33cfb87
Merge branch 'main' into implement-profile-picture-#343
Dun-sin Oct 30, 2023
ccf3702
Add Google Cloud Vision API setup to README #558
hubsMIT1 Oct 30, 2023
3a56d04
Merge branch 'implement-profile-picture-#343' of https://github.com/h…
hubsMIT1 Oct 30, 2023
a0680c1
Merge branch 'implement-profile-picture-#343' of https://github.com/h…
hubsMIT1 Oct 30, 2023
8741ce0
changed back the domain to origin #558
hubsMIT1 Oct 30, 2023
27e6404
Add Google update: Cloud Vision API setup to README
hubsMIT1 Oct 31, 2023
176b29b
Merge branch 'main' into implement-profile-picture-#343
hubsMIT1 Nov 1, 2023
70e2f26
added steps to setup Cloud Vision API
hubsMIT1 Nov 1, 2023
2f0bebc
Merge branch 'implement-profile-picture-#343' of https://github.com/h…
hubsMIT1 Nov 1, 2023
fb09f8c
Update userController.js
hubsMIT1 Nov 6, 2023
cc08da7
Used the nsfwjs library for image verification, removed cloud vision …
hubsMIT1 Nov 6, 2023
d4fa078
used useRef
hubsMIT1 Nov 6, 2023
2c3d10d
removed gcv packages,
hubsMIT1 Nov 7, 2023
7dd8889
Merge branch 'main' into implement-profile-picture-#343
Dun-sin Nov 7, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions client/src/components/UserAvatar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { HiUserCircle, HiPencil } from 'react-icons/hi';
import PropTypes from 'prop-types';


export default function UserAvatar({imageUrl, onUploadClick, onEditClick }) {
return (
<div className="relative group">
{imageUrl ? (
<>
<img
src={imageUrl}
alt="Profile Image"
className="h-20 w-20 rounded-full"
/>
<div className="absolute top-0 -right-5 opacity-0 group-hover:opacity-100 flex items-center space-x-2">
<HiPencil
className="text-blue-500 h-6 w-6 cursor-pointer"
onClick={onEditClick}
/>
</div>
</>
) : (
<HiUserCircle
className="text-highlight h-20 w-20 cursor-pointer"
onClick={onUploadClick}
title='upload profile image'
/>
)}
</div>
)
};

UserAvatar.propTypes = {
imageUrl: PropTypes.string.isRequired,
onUploadClick: PropTypes.func.isRequired,
onEditClick: PropTypes.func.isRequired,
};
69 changes: 54 additions & 15 deletions client/src/pages/Profile.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react';

import { HiUserCircle } from 'react-icons/hi';
import UserAvatar from '../components/UserAvatar';
import { useKindeAuth } from '@kinde-oss/kinde-auth-react';

import { useAuth } from 'context/AuthContext';
Expand All @@ -12,6 +12,8 @@ import SignupAnonUser from './SignupAnonUser';
const Profile = () => {
const [username, setUsername] = useState('Anonymous');
const [profileResponse, setProfileResponse] = useState();
const [uploadedImageUrl, setUploadedImageUrl] = useState(null);
const [imageFile, setImageFile] = useState(null);

const { authState, dispatchAuth } = useAuth();
const { logout } = useKindeAuth();
Expand All @@ -25,9 +27,12 @@ const Profile = () => {
const getProfileData = async (email) => {
try {
const response = await api.get(`/profile/${email}`);
const { aboutMe, age, gender, username } = response.data;
const { aboutMe, age, gender, username, profileImage } = response.data;

setUsername(username);
if (profileImage) {
setUploadedImageUrl(profileImage);
}
aboutRef.current.value = aboutMe || '';
ageRef.current.value = age;
genderRef.current.value = gender;
Expand All @@ -38,22 +43,52 @@ const Profile = () => {

const handleUserName = (e) => setUsername(e.target.value);

const handleUpdateProfile = async () => {
const data = {
email,
username,
aboutMe: aboutRef.current.value,
gender: genderRef.current.value,
age: Number(ageRef.current.value),
const handleImageUpload = () => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.onchange = (e) => {
const file = e.target.files[0];
if (file) {
setImageFile(file);
const reader = new FileReader();
reader.onload = (event) => {
setUploadedImageUrl(event.target.result);
};
reader.readAsDataURL(file);
}
};

fileInput.click();
};
const handleEditProfile = () => {
handleImageUpload();
};
const handleUpdateProfile = async () => {
const formData = new FormData();
formData.append('email', email);
formData.append('username', username);
formData.append('aboutMe', aboutRef.current.value);
formData.append('gender', genderRef.current.value);
formData.append('age', Number(ageRef.current.value));

if (imageFile) {
formData.append('profileImage', imageFile);
}
try {
const response = await api.post('/profile', data);
const response = await api.post('/profile', formData);
Dun-sin marked this conversation as resolved.
Show resolved Hide resolved
console.log(response.data.message);
setProfileResponse(response.data);
setProfileResponse(response.data.message);

} catch (error) {
console.error(error);
// Handle network errors or other unexpected issues
console.error('Error uploading file:', error);
if (error.response) {
// Handle HTTP errors with custom messages
console.log(error.response.data.error);
setProfileResponse(error.response.data.error);
}
}

};

function logOut() {
Expand Down Expand Up @@ -104,7 +139,11 @@ const Profile = () => {
) : (
<>
<section className="min-w-[300px] max-w-[400px] w-[40%] px-10 py-8 rounded-2xl flex flex-col justify-center items-center bg-clip-padding backdrop-filter backdrop-blur-2xl bg-gray-100 dark:bg-opacity-5 dark:bg-gray-300">
<HiUserCircle className="text-highlight h-20 w-20" />
<UserAvatar
imageUrl={uploadedImageUrl}
onUploadClick={handleImageUpload}
onEditClick={handleEditProfile}
/>

<input
className="outline-none bg-transparent w-fit text-center text-2xl placeholder:text-2xl placeholder:text-white"
Expand Down Expand Up @@ -169,7 +208,7 @@ const Profile = () => {
</button>
{profileResponse ? (
<div>
<p className="text-green-300">Profile Updated!</p>
<p className="text-green-300">{profileResponse}</p>
</div>
) : null}
</>
Expand Down
1 change: 1 addition & 0 deletions server/.env_sample
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ THIS IS JUST A FILE TO HELP YOU KNOW WHAT IS NEEDED IN THE .env FILE.
YOU HAVE TO CREATE ANOTHER FILE AND NAME IT .env

MongoDB_URL=mongodb://username:password@localhost:27018/
GOOGLE_APPLICATION_CREDENTIALS=''
Dun-sin marked this conversation as resolved.
Show resolved Hide resolved
hubsMIT1 marked this conversation as resolved.
Show resolved Hide resolved
72 changes: 71 additions & 1 deletion server/controllers/userController.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ const UserRouter = require('express').Router();
const validator = require('validator').default;
const { v4: uuidv4 } = require('uuid');

const multer = require('multer');
// google cloud vision api to check profile image validation
const vision = require('@google-cloud/vision');
// multer for profile image
const storage = multer.memoryStorage();
const imageUpload = multer({ storage: storage });

const User = require('../models/UserSchema');

let accessToken = process.env.ACCESS_TOKEN;
Expand Down Expand Up @@ -36,6 +43,50 @@ const emailValidator = (req, res, next) => {
next();
}
};
// check picture is safe or not
const profileImageValidator = async (imageBuffer) => {
Dun-sin marked this conversation as resolved.
Show resolved Hide resolved
const CREDENTIALS = JSON.parse(process.env.GOOGLE_APPLICATION_CREDENTIALS);
const CONFIG = {
credentials: {
private_key: CREDENTIALS.private_key,
client_email: CREDENTIALS.client_email,
},
};
const client = new vision.ImageAnnotatorClient(CONFIG);

const likelihoodToPercentage = {
VERY_UNLIKELY: 0,
UNLIKELY: 25,
POSSIBLE: 50,
LIKELY: 75,
VERY_LIKELY: 100,
};
const isUnsafe = (detections) => {
const likelihood =Math.max(
detections.adult,
detections.medical,
detections.spoof,
detections.violence,
detections.racy
);
return likelihoodToPercentage[likelihood] >= 50; // Adjust as needed
};

let result;
try {
[result] = await client.safeSearchDetection(imageBuffer);
} catch (err) {
console.error('cloud vision api error', err.code, ' : ', err.details);
return {
unsafe: false,
error:
'Currently, there is some error while uploading the profile image. Please try again later.',
};
}
const detections = result.safeSearchAnnotation;
const unsafe = isUnsafe(detections);
return { unsafe, error: null };
};

const getAccessToken = async () => {
try {
Expand Down Expand Up @@ -192,12 +243,31 @@ const updateProfile = async (req, res) => {
return res.status(NOT_FOUND).json({ error: 'User not found' });
}

if (req.file) {
const imageBuffer = req.file.buffer;
// Check for unsafe content using the utility function
const validationResult = await profileImageValidator(imageBuffer);
if (validationResult.unsafe) {
// Return the error if the image is not safe or there's a permission issue
return res
.status(NOT_ACCEPTABLE)
.json({ error: 'Unsafe content detected in the profile image' });
} else if (validationResult.error) {
console.error(error);
return res
.status(INTERNAL_SERVER_ERROR)
.json({ error: validationResult.error });
}
}
// Update user's profile with provided fields or the User fields or defaults
user.username = username || user.username || 'Anonymous';
user.aboutMe = aboutMe || user.aboutMe || null;
user.gender = gender || user.gender || 'Unknown';
user.age = age || user.age || null;
user.settings = settings || user.settings;
user.profileImage = req.file
? `data:${req.file.mimetype};base64,${req.file.buffer.toString('base64')}`
: user.profileImage;

// Save the updated user profile
await user.save();
Expand Down Expand Up @@ -235,7 +305,7 @@ const deleteUser = async (req, res) => {
};

UserRouter.route('/login').post(emailValidator, loginUser);
UserRouter.route('/profile').post(emailValidator, updateProfile);
UserRouter.route('/profile').post(imageUpload.single('profileImage'), emailValidator, updateProfile);
UserRouter.route('/profile/:email').get(getProfile);
UserRouter.route('/deleteUser').delete(emailValidator, deleteUser); //Email validation applied to the required request handlers

Expand Down
4 changes: 4 additions & 0 deletions server/models/UserSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ const UserSchema = new mongoose.Schema({
type: Object,
required: false,
},
profileImage: {
type: String,
required: false
},
});

UserSchema.index({ email: 1 }, { unique: true });
Expand Down
Loading
Loading