Skip to content

Commit

Permalink
use multipart/form-data for register
Browse files Browse the repository at this point in the history
  • Loading branch information
Shubham-Lal committed Apr 8, 2024
1 parent 5080a68 commit cc1fc2f
Show file tree
Hide file tree
Showing 12 changed files with 281 additions and 115 deletions.
11 changes: 10 additions & 1 deletion client/src/components/modal/auth-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,21 @@ import SignupTab from "./signup-tab";
import BriefInfo from "./brief-info";
import { IoClose } from "react-icons/io5";

type RegisterData = {
email: string;
password: string;
avatar: string | File;
username: string;
first_name: string;
last_name: string;
};

const AuthModal = () => {
const location = useLocation();
const { authTab, setAuthTab } = useAuthStore();
const { tempUser } = useTempStore();

const [registerData, setRegisterData] = useState(() => ({
const [registerData, setRegisterData] = useState<RegisterData>(() => ({
email: tempUser.email || "",
password: "",
avatar: tempUser.avatar || "",
Expand Down
68 changes: 42 additions & 26 deletions client/src/components/modal/brief-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useState, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { AuthStatus, AuthTab, useAuthStore, useTempStore } from "../../store/useAuthStore";
import { RegisterDataProps } from "../../types";
import useFileHandler from "../../hooks/useFileHandler";
import { toast } from "sonner";
import { LoadingSVG } from "../loading/svg";
import { IoPersonCircleOutline } from "react-icons/io5";
Expand All @@ -26,6 +25,7 @@ const BriefInfo: React.FC<RegisterDataProps> = ({ registerData, setRegisterData

const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
// eslint-disable-next-line no-useless-escape
const specialCharRegex = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/;
if (specialCharRegex.test(value)) {
return toast.warning('Special characters not allowed.');
Expand All @@ -42,15 +42,22 @@ const BriefInfo: React.FC<RegisterDataProps> = ({ registerData, setRegisterData
}));
}, [setRegisterData]);

const handleAvatarChange = useFileHandler(5);
const handleFileInputChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const base64String = await handleAvatarChange(e);
setRegisterData(prevState => ({
...prevState,
avatar: base64String || ""
}));
const handleFileInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files.length > 0) {
const file = event.target.files[0];
if (file) {
setRegisterData(prevState => ({
...prevState,
avatar: file
}));
}
}
};

const handleKeyPress = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === ' ') e.preventDefault();
}, []);

const handleFormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsSubmitted(true);
Expand All @@ -76,19 +83,18 @@ const BriefInfo: React.FC<RegisterDataProps> = ({ registerData, setRegisterData

if (trimmedUsername && trimmedFirstName && trimmedLastName) {
setLoading(true);

const formData = new FormData();
formData.append('email', registerData.email);
formData.append('password', registerData.password);
formData.append('username', trimmedUsername);
formData.append('first_name', trimmedFirstName);
formData.append('last_name', trimmedLastName);
formData.append('avatar', registerData.avatar);

await fetch(`${import.meta.env.VITE_SERVER_URL}/api/auth/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: registerData.email,
password: registerData.password,
avatar: registerData.avatar,
username: trimmedUsername,
first_name: trimmedFirstName,
last_name: trimmedLastName
})
body: formData
})
.then(res => res.json())
.then(response => {
Expand Down Expand Up @@ -128,8 +134,7 @@ const BriefInfo: React.FC<RegisterDataProps> = ({ registerData, setRegisterData
<div className='avatar-username__container'>
<div
className='avatar__container'
// onClick={() => document.getElementById('user-avatar')?.click()}
onClick={() => toast.warning('Feature coming soon.')}
onClick={() => document.getElementById('user-avatar')?.click()}
>
<input
type="file"
Expand All @@ -138,11 +143,19 @@ const BriefInfo: React.FC<RegisterDataProps> = ({ registerData, setRegisterData
onChange={handleFileInputChange}
/>
{registerData.avatar ? (
<img
src={registerData.avatar}
alt="Avatar"
style={{ width: '50px', height: '50px', objectFit: 'cover', border: '2px solid var(--body_color)', borderRadius: '50%' }}
/>
typeof registerData.avatar === 'string' ? (
<img
src={registerData.avatar}
alt="Avatar"
style={{ width: '50px', height: '50px', objectFit: 'cover', border: '2px solid var(--body_color)', borderRadius: '50%' }}
/>
) : (
<img
src={URL.createObjectURL(registerData.avatar)}
alt="Avatar"
style={{ width: '50px', height: '50px', objectFit: 'cover', border: '2px solid var(--body_color)', borderRadius: '50%' }}
/>
)
) : <IoPersonCircleOutline size={50} />}
<div className='upload-btn'>
{registerData.avatar ? <MdModeEdit size={15} /> : <GrCloudUpload size={15} />}
Expand All @@ -155,6 +168,7 @@ const BriefInfo: React.FC<RegisterDataProps> = ({ registerData, setRegisterData
name="username"
value={registerData.username}
onChange={handleInputChange}
onKeyPress={handleKeyPress}
className={`${isSubmitted && !validationState.isUsernameValid ? "shake" : ""}`}
style={{ borderColor: isSubmitted && !validationState.isUsernameValid ? "red" : "" }}
placeholder={isSubmitted && !validationState.isUsernameValid ? 'Required' : ''}
Expand All @@ -168,6 +182,7 @@ const BriefInfo: React.FC<RegisterDataProps> = ({ registerData, setRegisterData
name="first_name"
value={registerData.first_name}
onChange={handleInputChange}
onKeyPress={handleKeyPress}
className={`${isSubmitted && !validationState.isFirstNameValid ? "shake" : ""}`}
style={{ borderColor: isSubmitted && !validationState.isFirstNameValid ? "red" : "" }}
placeholder={isSubmitted && !validationState.isFirstNameValid ? 'Required' : ''}
Expand All @@ -180,6 +195,7 @@ const BriefInfo: React.FC<RegisterDataProps> = ({ registerData, setRegisterData
name="last_name"
value={registerData.last_name}
onChange={handleInputChange}
onKeyPress={handleKeyPress}
className={`${isSubmitted && !validationState.isLastNameValid ? "shake" : ""}`}
style={{ borderColor: isSubmitted && !validationState.isLastNameValid ? "red" : "" }}
placeholder={isSubmitted && !validationState.isLastNameValid ? 'Required' : ''}
Expand Down
4 changes: 2 additions & 2 deletions client/src/pages/auth/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default function AuthPage() {
setAuthTab(type === 'login' ? AuthTab.Login : type === 'signup' ? AuthTab.Signup : AuthTab.Login);
}
else if (isAuthenticated === AuthStatus.Authenticated) {
navigate(route === '/auth' || route === '/login' || route === '/signup' ? '/' : route)
navigate(route === '/auth' || route === '/login' || route === '/signup' ? '/' : route, { replace: true });
}
else if (isAuthenticated === AuthStatus.Failed) {
setAuthTab(type === 'login' ? AuthTab.Login : type === 'signup' ? AuthTab.Signup : AuthTab.Login);
Expand All @@ -36,7 +36,7 @@ export default function AuthPage() {
avatar: user.picture || ""
});
setAuthTab(AuthTab.Signup);
navigate('/auth', { replace: true });
navigate('/auth?type=signup', { replace: true });
}
}, [isAuthenticated, location.search, navigate, route, setAuthTab, setTempUser]);

Expand Down
8 changes: 3 additions & 5 deletions client/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import React from "react";

export interface RegisterDataProps {
registerData: {
email: string;
password: string;
avatar: string;
avatar: string | File;
username: string;
first_name: string;
last_name: string;
};
setRegisterData: React.Dispatch<React.SetStateAction<{
email: string;
password: string;
avatar: string;
avatar: string | File;
username: string;
first_name: string;
last_name: string;
}>>;
}
}
4 changes: 0 additions & 4 deletions client/src/utils/handleAutoLogin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { toast } from 'sonner';
import { User, AuthStatus, AuthTab } from '../store/useAuthStore';

type SetRoute = (navigate: string) => void;
Expand All @@ -24,19 +23,16 @@ const handleAutoLogin = (setRoute: SetRoute, setUser: SetUser, setIsAuthenticate
.then(res => res.json())
.then(response => {
if (response.success) {
toast.success(response.message);
setUser(response.data.user);
setIsAuthenticated(AuthStatus.Authenticated);
setAuthTab(AuthTab.Closed);
}
else {
toast.success('Session logged out.');
setIsAuthenticated(AuthStatus.Failed);
localStorage.removeItem('token');
}
})
.catch(() => {
toast.success('Session logged out.');
setIsAuthenticated(AuthStatus.Failed);
localStorage.removeItem('token');
});
Expand Down
1 change: 0 additions & 1 deletion server/avatars/README.md

This file was deleted.

53 changes: 13 additions & 40 deletions server/controllers/auth.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const fs = require('fs');
const path = require('path');
const { getMimeType } = require('../utils/getMimeType');
const ErrorHandler = require('../utils/ErrorHandler');

exports.handleGoogleAuth = async function (fastify, request, reply) {
Expand All @@ -26,43 +23,33 @@ exports.handleGoogleAuth = async function (fastify, request, reply) {

exports.register = async function (fastify, request, reply) {
try {
const { email, password, avatar, username, first_name, last_name } = request.body;
const { email, password, username, first_name, last_name } = request.body;
let avatar = null;

if (request.body.avatar) {
avatar = request.body.avatar;
}
else if (request.file) {
avatar = request.file;
}

const [emailExists] = await fastify.mysql.query('SELECT * FROM users WHERE email = ?', [email]);
if (emailExists.length > 0) throw new ErrorHandler(400, false, 'Account already exists.');

const [usernameExists] = await fastify.mysql.query('SELECT * FROM users WHERE username = ?', [username]);
if (usernameExists.length > 0) throw new ErrorHandler(400, false, 'Username already exists.');

let avatarPath = null;
if (avatar) {
const isBase64 = avatar.startsWith('data:image');
if (!isBase64) avatarPath = avatar;
else {
const mimeType = getMimeType(avatar);
if (!mimeType) throw new ErrorHandler(400, false, 'Invalid avatar format.');

const base64Data = avatar.replace(/^data:image\/\w+;base64,/, '');
const extension = mimeType.split('/')[1];
const timestamp = new Date().toISOString().replace(/:/g, '-').replace('T', '_').split('.')[0];
const uniqueFilename = `${username}_${timestamp}.${extension}`;
const fullPath = path.resolve(__dirname, '..', 'avatars', uniqueFilename);
fs.writeFileSync(fullPath, base64Data, 'base64');
avatarPath = uniqueFilename;
}
}

const hashedPassword = await bcrypt.hash(password, 10);

const [result] = await fastify.mysql.query('INSERT INTO users (email, password, username, first_name, last_name, avatar) VALUES (?, ?, ?, ?, ?, ?)', [email, hashedPassword, username, first_name, last_name, avatarPath]);
const [result] = await fastify.mysql.query('INSERT INTO users (email, password, username, first_name, last_name, avatar) VALUES (?, ?, ?, ?, ?, ?)', [email, hashedPassword, username, first_name, last_name, null]);

if (result.affectedRows > 0) {
const token = jwt.sign({ userId: username }, process.env.JWT_SECRET, { expiresIn: '12h' });
return reply.code(201).send({
success: true,
message: 'Account created successfully.',
data: {
user: { email, avatar: avatarPath, username, first_name, last_name },
user: { email, avatar: null, username, first_name, last_name },
token
}
});
Expand Down Expand Up @@ -98,21 +85,14 @@ exports.login = async function (fastify, request, reply) {

if (!isPasswordValid) throw new ErrorHandler(400, false, 'Incorrect password.');

let avatarUrl = null;
if (userData.avatar) {
if (userData.avatar.startsWith('http')) avatarUrl = userData.avatar;
else avatarUrl = `${process.env.SERVER_URL}/avatars/${userData.avatar}`;
}

const token = jwt.sign({ userId: userData.username }, process.env.JWT_SECRET, { expiresIn: '12h' });

return reply.code(200).send({
success: true,
message: 'Login successful.',
data: {
user: {
...(({ password, ...rest }) => rest)(userData),
avatar: avatarUrl
...(({ password, ...rest }) => rest)(userData)
},
token
}
Expand Down Expand Up @@ -143,19 +123,12 @@ exports.autoLogin = async function (fastify, request, reply) {

const userData = user[0];

let avatarUrl = null;
if (userData.avatar) {
if (userData.avatar.startsWith('http')) avatarUrl = userData.avatar;
else avatarUrl = `${process.env.SERVER_URL}/avatars/${userData.avatar}`;
}

return reply.code(200).send({
success: true,
message: 'Login successful.',
data: {
user: {
...(({ password, ...rest }) => rest)(userData),
avatar: avatarUrl
...(({ password, ...rest }) => rest)(userData)
}
}
});
Expand Down
Loading

0 comments on commit cc1fc2f

Please sign in to comment.