From fdb602eeac83a22ad37beab99e049114f30a763c Mon Sep 17 00:00:00 2001 From: Nairuz yasser <84462600+NairuzY@users.noreply.github.com> Date: Sun, 12 Nov 2023 23:43:55 +0200 Subject: [PATCH] doctor upload docs (#128) --- .../src/app/controllers/auth.controller.ts | 14 +- backend/src/app/services/auth.service.ts | 27 +- clinic-common/types/doctor.types.ts | 29 +- clinic-common/validators/doctor.validator.ts | 18 -- frontend/src/api/auth.ts | 9 - .../features/auth/routes/RequestDoctor.tsx | 279 ++++++++++++++++-- 6 files changed, 306 insertions(+), 70 deletions(-) diff --git a/backend/src/app/controllers/auth.controller.ts b/backend/src/app/controllers/auth.controller.ts index a6a28f3f..f94cfd47 100644 --- a/backend/src/app/controllers/auth.controller.ts +++ b/backend/src/app/controllers/auth.controller.ts @@ -25,12 +25,17 @@ import { GetUserByUsernameResponse, type UserType, } from 'clinic-common/types/user.types' -import { RegisterDoctorRequestValidator } from 'clinic-common/validators/doctor.validator' + import { type DoctorStatus, RegisterDoctorRequestResponse, } from 'clinic-common/types/doctor.types' import { getModelIdForUsername } from '../services/auth.service' +import multer from 'multer' + +const storage = multer.memoryStorage() // You can choose a different storage method + +const upload = multer({ storage }) export const authRouter = Router() @@ -97,9 +102,12 @@ authRouter.get( // Submit a Request to Register as a Doctor authRouter.post( '/request-doctor', - validate(RegisterDoctorRequestValidator), + upload.array('documents', 50), asyncWrapper(async (req, res) => { - const doctor = await submitDoctorRequest(req.body) + const doctor = await submitDoctorRequest({ + ...req.body, + documents: req.files as Express.Multer.File[], + }) res.send( new RegisterDoctorRequestResponse( doctor.id, diff --git a/backend/src/app/services/auth.service.ts b/backend/src/app/services/auth.service.ts index 8f034c0f..258cbf29 100644 --- a/backend/src/app/services/auth.service.ts +++ b/backend/src/app/services/auth.service.ts @@ -14,14 +14,16 @@ import { type HydratedDocument } from 'mongoose' import { PatientModel } from '../models/patient.model' import { DoctorStatus, - type RegisterDoctorRequest, + type IRegisterDoctorRequest, } from 'clinic-common/types/doctor.types' import { type DoctorDocument, DoctorModel } from '../models/doctor.model' import { hash } from 'bcrypt' import { type WithUser } from '../utils/typeUtils' import { AppointmentModel } from '../models/appointment.model' import { AdminModel } from '../models/admin.model' - +import FireBase from '../../../../firebase.config' +import { getStorage, ref, uploadBytes } from 'firebase/storage' +import { getDownloadURL } from 'firebase/storage' const jwtSecret = process.env.JWT_TOKEN ?? 'secret' const bcryptSalt = process.env.BCRYPT_SALT ?? '$2b$10$13bXTGGukQXsCf5hokNe2u' @@ -149,8 +151,10 @@ export async function getUserByUsername( } export async function submitDoctorRequest( - doctor: RegisterDoctorRequest + doctor: IRegisterDoctorRequest ): Promise> { + console.log(doctor) + if (await isUsernameTaken(doctor.username)) { throw new UsernameAlreadyTakenError() } @@ -167,17 +171,30 @@ export async function submitDoctorRequest( type: UserType.Doctor, }) await user.save() + const documentsPaths: string[] = [] + const storage = getStorage(FireBase) + const storageRef = ref(storage, 'doctors/') + + for (let i = 0; i < doctor.documents.length; i++) { + const fileRef = ref(storageRef, doctor.name + [i]) + await uploadBytes(fileRef, doctor.documents[i].buffer, { + contentType: doctor.documents[i].mimetype, + }) + const fullPath = await getDownloadURL(fileRef) + documentsPaths.push(fullPath.toString()) + } + const newDoctor = await DoctorModel.create({ user: user.id, name: doctor.name, email: doctor.email, dateOfBirth: doctor.dateOfBirth, - hourlyRate: doctor.hourlyRate, + hourlyRate: parseInt(doctor.hourlyRate), affiliation: doctor.affiliation, educationalBackground: doctor.educationalBackground, speciality: doctor.speciality, requestStatus: DoctorStatus.Pending, - documents: doctor.documents, + documents: documentsPaths, }) await newDoctor.save() diff --git a/clinic-common/types/doctor.types.ts b/clinic-common/types/doctor.types.ts index 7f56e4fd..ed03b21f 100644 --- a/clinic-common/types/doctor.types.ts +++ b/clinic-common/types/doctor.types.ts @@ -1,7 +1,6 @@ import type { z } from 'zod' import type { UpdateDoctorRequestValidator, - RegisterDoctorRequestValidator, AddAvailableTimeSlotsRequestValidator, } from '../validators/doctor.validator' @@ -172,10 +171,6 @@ export class GetApprovedDoctorsResponse { export type UpdateDoctorRequest = z.infer -export type RegisterDoctorRequest = z.infer< - typeof RegisterDoctorRequestValidator -> - export type AddAvailableTimeSlotsRequest = z.infer< typeof AddAvailableTimeSlotsRequestValidator > @@ -189,3 +184,27 @@ export class AddAvailableTimeSlotsResponse extends GetApprovedDoctorResponse {} export class GetWalletMoneyResponse { constructor(public money: number) {} } + +export type IRegisterDoctorRequest = { + username: string + password: string + name: string + email: string + mobileNumber: string + dateOfBirth: Date + hourlyRate: string + affiliation: string + educationalBackground: string + speciality: string + + documents: MulterFile[] +} + +type MulterFile = { + fieldname: string + originalname: string + encoding: string + mimetype: string + buffer: Buffer + size: number +} diff --git a/clinic-common/validators/doctor.validator.ts b/clinic-common/validators/doctor.validator.ts index 373dd44b..bef40990 100644 --- a/clinic-common/validators/doctor.validator.ts +++ b/clinic-common/validators/doctor.validator.ts @@ -12,21 +12,3 @@ export const UpdateDoctorRequestValidator = zod.object({ export const AddAvailableTimeSlotsRequestValidator = zod.object({ time: zod.coerce.date(), }) - -export const RegisterDoctorRequestValidator = zod.object({ - username: zod - .string() - .min(3) - .max(255) - .regex(/^[a-zA-Z0-9_]+$/), - password: zod.string().min(6).max(255), - name: zod.string().min(3).max(255), - email: zod.string().email(), - mobileNumber: zod.string().min(11).max(11), - dateOfBirth: zod.coerce.date(), - hourlyRate: zod.number(), - affiliation: zod.string().min(1), - educationalBackground: zod.string().min(1), - speciality: zod.string().min(1), - documents: zod.array(zod.string()), -}) diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts index 105b1231..3a0c74b5 100644 --- a/frontend/src/api/auth.ts +++ b/frontend/src/api/auth.ts @@ -5,7 +5,6 @@ import { LoginResponse, RegisterRequest, } from 'clinic-common/types/auth.types' -import { RegisterDoctorRequest } from 'clinic-common/types/doctor.types' export async function login(request: LoginRequest): Promise { return await api.post('/auth/login', request).then((res) => { @@ -21,14 +20,6 @@ export async function registerPatient(request: RegisterRequest): Promise { }) } -export async function requestDoctor( - request: RegisterDoctorRequest -): Promise { - return await api.post('/auth/request-doctor', request).then((res) => { - console.log('requested successfully', res.data) - }) -} - export async function getCurrentUser(): Promise { if (!localStorage.getItem('token')) { return Promise.reject('No token found') diff --git a/frontend/src/features/auth/routes/RequestDoctor.tsx b/frontend/src/features/auth/routes/RequestDoctor.tsx index 2fb951e4..28a9f37f 100644 --- a/frontend/src/features/auth/routes/RequestDoctor.tsx +++ b/frontend/src/features/auth/routes/RequestDoctor.tsx @@ -1,36 +1,255 @@ -import { ApiForm } from '@/components/ApiForm' -import { requestDoctor } from '@/api/auth' -import { useAuth } from '@/hooks/auth' - -import { RegisterDoctorRequestValidator } from 'clinic-common/validators/doctor.validator' -import { RegisterDoctorRequest } from 'clinic-common/types/doctor.types' +import { useState } from 'react' +import axios from 'axios' +import Button from '@mui/material/Button' +import SendIcon from '@mui/icons-material/Send' +import { ToastContainer, toast } from 'react-toastify' +import 'react-toastify/dist/ReactToastify.css' +import { + TextField, + Typography, + Grid, + RadioGroup, + Radio, + Box, + Container, + FormControlLabel, +} from '@mui/material' export const RequestDoctor = () => { - const { refreshUser } = useAuth() + const [name, setName] = useState('') + const [email, setEmail] = useState('') + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [dateOfBirth, setDateOfBirth] = useState('') + //mobile number + const [hourlyRate, setHourlyRate] = useState('') + const [affilation, setAffilation] = useState('') + const [educationalBackground, setEducationalBackground] = useState('') + const [speciality, setSpeciality] = useState('') - return ( - - fields={[ - { label: 'Username', property: 'username' }, - { label: 'Password', property: 'password' }, - { label: 'Name', property: 'name' }, - { label: 'Email', property: 'email' }, - { - label: 'Mobile Number', - property: 'mobileNumber', - customError: 'Mobile number must be 10 digits.', + const [fieldValue, setFieldValue] = useState({ files: [] } as any) + + async function submit(e: any) { + console.log('submit') + console.log(fieldValue.files) + + e.preventDefault() + + const formData = new FormData() + + formData.append('name', name) + formData.append('email', email) + formData.append('username', username) + formData.append('password', password) + formData.append('dateOfBirth', dateOfBirth) + formData.append('hourlyRate', hourlyRate) + formData.append('affiliation', affilation) + formData.append('educationalBackground', educationalBackground) + + formData.append('speciality', speciality) + + // formData.append('documents', fieldValue.files) + for (let i = 0; i < fieldValue.files.length; i++) { + formData.append('documents', fieldValue.files[i]) + } + + console.log(formData) + + await axios + .post('http://localhost:3000/auth/request-doctor', formData, { + headers: { + 'Content-Type': 'multipart/form-data; ${formData.getBoundary()}', // Axios sets the correct Content-Type header with the boundary. }, - { label: 'Date of Birth', property: 'dateOfBirth', type: 'date' }, - { label: 'Hourly Rate', property: 'hourlyRate', valueAsNumber: true }, - { label: 'Affiliation', property: 'affiliation' }, - { label: 'Educational background', property: 'educationalBackground' }, - { label: 'Speciality', property: 'speciality' }, - ]} - validator={RegisterDoctorRequestValidator} - successMessage="Register successfully." - action={requestDoctor} - onSuccess={() => refreshUser()} - buttonText="Register" - /> + }) + .then(() => { + toast.success('Your request has been sent successfully') + }) + .catch((err) => { + toast.error(err.response.data.message) + console.log(err) + }) + } + + return ( + + + + Register + + + +
+ + + { + setName(e.target.value) + }} + placeholder="Enter your Name" + required + /> + + + { + setEmail(e.target.value) + }} + placeholder="Enter your email address" + required + /> + + + { + setUsername(e.target.value) + }} + placeholder="Enter userrname" + required + /> + + + { + setPassword(e.target.value) + }} + placeholder="Enter password" + required + /> + + + + { + setDateOfBirth(e.target.value) + }} + placeholder="Enter date of birth" + required + /> + + + + { + setHourlyRate(e.target.value) + }} + placeholder="Enter hourly rate in $" + required + /> + + + { + setAffilation(e.target.value) + }} + placeholder="Enter affiliation" + required + /> + + + { + setSpeciality(e.target.value) + }} + placeholder="Enter your speciality" + /> + + + + + + { + setEducationalBackground(e.target.value) + }} + > + } + label="Associate degree" + /> + } + label="Bachelor's degree" + /> + } + label="Master's degree" + /> + } + label="Doctoral degree" + /> + + + + + { + if ( + event.currentTarget.files && + event.currentTarget.files.length > 0 + ) + setFieldValue({ files: event.currentTarget.files }) + }} + /> + + +
+ +
+
+
) } + +export default RequestDoctor