Merge pull request #249 from emrysal/bugfix/prevent-duplicate-usernames
This commit is contained in:
		
						commit
						28eec8ed7f
					
				
					 4 changed files with 103 additions and 47 deletions
				
			
		
							
								
								
									
										17
									
								
								components/ui/UsernameInput.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								components/ui/UsernameInput.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | |||
| import React from "react"; | ||||
| 
 | ||||
| export const UsernameInput = React.forwardRef( (props, ref) => ( | ||||
|   // todo, check if username is already taken here?
 | ||||
|   <div> | ||||
|     <label htmlFor="username" className="block text-sm font-medium text-gray-700"> | ||||
|       Username | ||||
|     </label> | ||||
|     <div className="mt-1 rounded-md shadow-sm flex"> | ||||
|       <span className="bg-gray-50 border border-r-0 border-gray-300 rounded-l-md px-3 inline-flex items-center text-gray-500 sm:text-sm"> | ||||
|         {typeof window !== "undefined" && window.location.hostname}/ | ||||
|       </span> | ||||
|       <input ref={ref} type="text" name="username" id="username" autoComplete="username" required defaultValue={props.defaultValue} | ||||
|              className="focus:ring-blue-500 focus:border-blue-500 flex-grow block w-full min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300"/> | ||||
|     </div> | ||||
|   </div> | ||||
| )); | ||||
							
								
								
									
										22
									
								
								components/ui/alerts/Error.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								components/ui/alerts/Error.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | |||
| 
 | ||||
| import { XCircleIcon } from '@heroicons/react/solid' | ||||
| 
 | ||||
| export default function ErrorAlert(props) { | ||||
|   return ( | ||||
|     <div className="rounded-md bg-red-50 p-4"> | ||||
|       <div className="flex"> | ||||
|         <div className="flex-shrink-0"> | ||||
|           <XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" /> | ||||
|         </div> | ||||
|         <div className="ml-3"> | ||||
|           <h3 className="text-sm font-medium text-red-800">Something went wrong</h3> | ||||
|           <div className="text-sm text-red-700"> | ||||
|             <p> | ||||
|               {props.message} | ||||
|             </p> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
|  | @ -3,46 +3,58 @@ import { getSession } from 'next-auth/client'; | |||
| import prisma from '../../../lib/prisma'; | ||||
| 
 | ||||
| export default async function handler(req: NextApiRequest, res: NextApiResponse) { | ||||
|     const session = await getSession({req: req}); | ||||
|   const session = await getSession({req: req}); | ||||
| 
 | ||||
|     if (!session) { | ||||
|         res.status(401).json({message: "Not authenticated"}); | ||||
|         return; | ||||
|   if (!session) { | ||||
|       res.status(401).json({message: "Not authenticated"}); | ||||
|       return; | ||||
|   } | ||||
| 
 | ||||
|   // Get user
 | ||||
|   const user = await prisma.user.findUnique({ | ||||
|     where: { | ||||
|       email: session.user.email, | ||||
|     }, | ||||
|     select: { | ||||
|       id: true, | ||||
|       password: true | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|     // Get user
 | ||||
|     const user = await prisma.user.findFirst({ | ||||
|         where: { | ||||
|             email: session.user.email, | ||||
|         }, | ||||
|         select: { | ||||
|             id: true, | ||||
|             password: true | ||||
|         } | ||||
|   if (!user) { res.status(404).json({message: 'User not found'}); return; } | ||||
| 
 | ||||
|   const username = req.body.username; | ||||
|   // username is changed: username is optional but it is necessary to be unique, enforce here
 | ||||
|   if (username !== user.username) { | ||||
|     const userConflict = await prisma.user.findFirst({ | ||||
|       where: { | ||||
|         username, | ||||
|       } | ||||
|     }); | ||||
|     if (userConflict) { | ||||
|       return res.status(409).json({ message: 'Username already taken' }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|     if (!user) { res.status(404).json({message: 'User not found'}); return; } | ||||
|   const name = req.body.name; | ||||
|   const description = req.body.description; | ||||
|   const avatar = req.body.avatar; | ||||
|   const timeZone = req.body.timeZone; | ||||
|   const weekStart = req.body.weekStart; | ||||
| 
 | ||||
|     const username = req.body.username; | ||||
|     const name = req.body.name; | ||||
|     const description = req.body.description; | ||||
|     const avatar = req.body.avatar; | ||||
|     const timeZone = req.body.timeZone; | ||||
|     const weekStart = req.body.weekStart; | ||||
|   const updateUser = await prisma.user.update({ | ||||
|     where: { | ||||
|       id: user.id, | ||||
|     }, | ||||
|     data: { | ||||
|       username, | ||||
|       name, | ||||
|       avatar, | ||||
|       bio: description, | ||||
|       timeZone: timeZone, | ||||
|       weekStart: weekStart, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|     const updateUser = await prisma.user.update({ | ||||
|         where: { | ||||
|           id: user.id, | ||||
|         }, | ||||
|         data: { | ||||
|           username, | ||||
|           name, | ||||
|           avatar, | ||||
|           bio: description, | ||||
|           timeZone: timeZone, | ||||
|           weekStart: weekStart, | ||||
|         }, | ||||
|     }); | ||||
| 
 | ||||
|     res.status(200).json({message: 'Profile updated successfully'}); | ||||
|   return res.status(200).json({message: 'Profile updated successfully'}); | ||||
| } | ||||
|  | @ -9,6 +9,8 @@ import SettingsShell from '../../components/Settings'; | |||
| import Avatar from '../../components/Avatar'; | ||||
| import { signIn, useSession, getSession } from 'next-auth/client'; | ||||
| import TimezoneSelect from 'react-timezone-select'; | ||||
| import {UsernameInput} from "../../components/ui/UsernameInput"; | ||||
| import ErrorAlert from "../../components/ui/alerts/Error"; | ||||
| 
 | ||||
| export default function Settings(props) { | ||||
|     const [ session, loading ] = useSession(); | ||||
|  | @ -22,12 +24,22 @@ export default function Settings(props) { | |||
|     const [ selectedTimeZone, setSelectedTimeZone ] = useState({ value: props.user.timeZone }); | ||||
|     const [ selectedWeekStartDay, setSelectedWeekStartDay ] = useState(props.user.weekStart || 'Sunday'); | ||||
| 
 | ||||
|     const [ hasErrors, setHasErrors ] = useState(false); | ||||
|     const [ errorMessage, setErrorMessage ] = useState(''); | ||||
| 
 | ||||
|     if (loading) { | ||||
|         return <p className="text-gray-400">Loading...</p>; | ||||
|     } | ||||
| 
 | ||||
|     const closeSuccessModal = () => { setSuccessModalOpen(false); } | ||||
| 
 | ||||
|     const handleError = async (resp) => { | ||||
|       if (!resp.ok) { | ||||
|         const error = await resp.json(); | ||||
|         throw new Error(error.message); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     async function updateProfileHandler(event) { | ||||
|         event.preventDefault(); | ||||
| 
 | ||||
|  | @ -46,10 +58,10 @@ export default function Settings(props) { | |||
|             headers: { | ||||
|                 'Content-Type': 'application/json' | ||||
|             } | ||||
|         }).then(handleError).then( () => setSuccessModalOpen(true) ).catch( (err) => { | ||||
|           setHasErrors(true); | ||||
|           setErrorMessage(err.message); | ||||
|         }); | ||||
| 
 | ||||
|         router.replace(router.asPath); | ||||
|         setSuccessModalOpen(true); | ||||
|     } | ||||
| 
 | ||||
|     return( | ||||
|  | @ -60,6 +72,7 @@ export default function Settings(props) { | |||
|             </Head> | ||||
|             <SettingsShell> | ||||
|                 <form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={updateProfileHandler}> | ||||
|                     {hasErrors && <ErrorAlert message={errorMessage} />} | ||||
|                     <div className="py-6 px-4 sm:p-6 lg:pb-8"> | ||||
|                         <div> | ||||
|                             <h2 className="text-lg leading-6 font-medium text-gray-900">Profile</h2> | ||||
|  | @ -72,15 +85,7 @@ export default function Settings(props) { | |||
|                             <div className="flex-grow space-y-6"> | ||||
|                                 <div className="flex"> | ||||
|                                     <div className="w-1/2 mr-2"> | ||||
|                                         <label htmlFor="username" className="block text-sm font-medium text-gray-700"> | ||||
|                                             Username | ||||
|                                         </label> | ||||
|                                         <div className="mt-1 rounded-md shadow-sm flex"> | ||||
|                                             <span className="bg-gray-50 border border-r-0 border-gray-300 rounded-l-md px-3 inline-flex items-center text-gray-500 sm:text-sm"> | ||||
|                                                 {window.location.hostname}/ | ||||
|                                             </span> | ||||
|                                             <input ref={usernameRef} type="text" name="username" id="username" autoComplete="username" required className="focus:ring-blue-500 focus:border-blue-500 flex-grow block w-full min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300" defaultValue={props.user.username} /> | ||||
|                                         </div> | ||||
|                                         <UsernameInput ref={usernameRef} defaultValue={props.user.username} /> | ||||
|                                     </div> | ||||
|                                     <div className="w-1/2 ml-2"> | ||||
|                                         <label htmlFor="name" className="block text-sm font-medium text-gray-700">Full name</label> | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue
	
	 Bailey Pumfleet
						Bailey Pumfleet