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'; | import prisma from '../../../lib/prisma'; | ||||||
| 
 | 
 | ||||||
| export default async function handler(req: NextApiRequest, res: NextApiResponse) { | export default async function handler(req: NextApiRequest, res: NextApiResponse) { | ||||||
|     const session = await getSession({req: req}); |   const session = await getSession({req: req}); | ||||||
| 
 | 
 | ||||||
|     if (!session) { |   if (!session) { | ||||||
|         res.status(401).json({message: "Not authenticated"}); |       res.status(401).json({message: "Not authenticated"}); | ||||||
|         return; |       return; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Get user
 | ||||||
|  |   const user = await prisma.user.findUnique({ | ||||||
|  |     where: { | ||||||
|  |       email: session.user.email, | ||||||
|  |     }, | ||||||
|  |     select: { | ||||||
|  |       id: true, | ||||||
|  |       password: true | ||||||
|     } |     } | ||||||
|  |   }); | ||||||
| 
 | 
 | ||||||
|     // Get user
 |   if (!user) { res.status(404).json({message: 'User not found'}); return; } | ||||||
|     const user = await prisma.user.findFirst({ | 
 | ||||||
|         where: { |   const username = req.body.username; | ||||||
|             email: session.user.email, |   // username is changed: username is optional but it is necessary to be unique, enforce here
 | ||||||
|         }, |   if (username !== user.username) { | ||||||
|         select: { |     const userConflict = await prisma.user.findFirst({ | ||||||
|             id: true, |       where: { | ||||||
|             password: true |         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 updateUser = await prisma.user.update({ | ||||||
|     const name = req.body.name; |     where: { | ||||||
|     const description = req.body.description; |       id: user.id, | ||||||
|     const avatar = req.body.avatar; |     }, | ||||||
|     const timeZone = req.body.timeZone; |     data: { | ||||||
|     const weekStart = req.body.weekStart; |       username, | ||||||
|  |       name, | ||||||
|  |       avatar, | ||||||
|  |       bio: description, | ||||||
|  |       timeZone: timeZone, | ||||||
|  |       weekStart: weekStart, | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
| 
 | 
 | ||||||
|     const updateUser = await prisma.user.update({ |   return res.status(200).json({message: 'Profile updated successfully'}); | ||||||
|         where: { |  | ||||||
|           id: user.id, |  | ||||||
|         }, |  | ||||||
|         data: { |  | ||||||
|           username, |  | ||||||
|           name, |  | ||||||
|           avatar, |  | ||||||
|           bio: description, |  | ||||||
|           timeZone: timeZone, |  | ||||||
|           weekStart: weekStart, |  | ||||||
|         }, |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     res.status(200).json({message: 'Profile updated successfully'}); |  | ||||||
| } | } | ||||||
|  | @ -9,6 +9,8 @@ import SettingsShell from '../../components/Settings'; | ||||||
| import Avatar from '../../components/Avatar'; | import Avatar from '../../components/Avatar'; | ||||||
| import { signIn, useSession, getSession } from 'next-auth/client'; | import { signIn, useSession, getSession } from 'next-auth/client'; | ||||||
| import TimezoneSelect from 'react-timezone-select'; | import TimezoneSelect from 'react-timezone-select'; | ||||||
|  | import {UsernameInput} from "../../components/ui/UsernameInput"; | ||||||
|  | import ErrorAlert from "../../components/ui/alerts/Error"; | ||||||
| 
 | 
 | ||||||
| export default function Settings(props) { | export default function Settings(props) { | ||||||
|     const [ session, loading ] = useSession(); |     const [ session, loading ] = useSession(); | ||||||
|  | @ -22,12 +24,22 @@ export default function Settings(props) { | ||||||
|     const [ selectedTimeZone, setSelectedTimeZone ] = useState({ value: props.user.timeZone }); |     const [ selectedTimeZone, setSelectedTimeZone ] = useState({ value: props.user.timeZone }); | ||||||
|     const [ selectedWeekStartDay, setSelectedWeekStartDay ] = useState(props.user.weekStart || 'Sunday'); |     const [ selectedWeekStartDay, setSelectedWeekStartDay ] = useState(props.user.weekStart || 'Sunday'); | ||||||
| 
 | 
 | ||||||
|  |     const [ hasErrors, setHasErrors ] = useState(false); | ||||||
|  |     const [ errorMessage, setErrorMessage ] = useState(''); | ||||||
|  | 
 | ||||||
|     if (loading) { |     if (loading) { | ||||||
|         return <p className="text-gray-400">Loading...</p>; |         return <p className="text-gray-400">Loading...</p>; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const closeSuccessModal = () => { setSuccessModalOpen(false); } |     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) { |     async function updateProfileHandler(event) { | ||||||
|         event.preventDefault(); |         event.preventDefault(); | ||||||
| 
 | 
 | ||||||
|  | @ -46,10 +58,10 @@ export default function Settings(props) { | ||||||
|             headers: { |             headers: { | ||||||
|                 'Content-Type': 'application/json' |                 'Content-Type': 'application/json' | ||||||
|             } |             } | ||||||
|  |         }).then(handleError).then( () => setSuccessModalOpen(true) ).catch( (err) => { | ||||||
|  |           setHasErrors(true); | ||||||
|  |           setErrorMessage(err.message); | ||||||
|         }); |         }); | ||||||
| 
 |  | ||||||
|         router.replace(router.asPath); |  | ||||||
|         setSuccessModalOpen(true); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return( |     return( | ||||||
|  | @ -60,6 +72,7 @@ export default function Settings(props) { | ||||||
|             </Head> |             </Head> | ||||||
|             <SettingsShell> |             <SettingsShell> | ||||||
|                 <form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={updateProfileHandler}> |                 <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 className="py-6 px-4 sm:p-6 lg:pb-8"> | ||||||
|                         <div> |                         <div> | ||||||
|                             <h2 className="text-lg leading-6 font-medium text-gray-900">Profile</h2> |                             <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-grow space-y-6"> | ||||||
|                                 <div className="flex"> |                                 <div className="flex"> | ||||||
|                                     <div className="w-1/2 mr-2"> |                                     <div className="w-1/2 mr-2"> | ||||||
|                                         <label htmlFor="username" className="block text-sm font-medium text-gray-700"> |                                         <UsernameInput ref={usernameRef} defaultValue={props.user.username} /> | ||||||
|                                             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> |  | ||||||
|                                     </div> |                                     </div> | ||||||
|                                     <div className="w-1/2 ml-2"> |                                     <div className="w-1/2 ml-2"> | ||||||
|                                         <label htmlFor="name" className="block text-sm font-medium text-gray-700">Full name</label> |                                         <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