diff --git a/client/src/AdminDashboard/TempAdminDashboardPage.tsx b/client/src/AdminDashboard/TempAdminDashboardPage.tsx index 8e542336..a496af39 100644 --- a/client/src/AdminDashboard/TempAdminDashboardPage.tsx +++ b/client/src/AdminDashboard/TempAdminDashboardPage.tsx @@ -243,4 +243,4 @@ function TempAdminDashboardPage() { ); } -export default TempAdminDashboardPage; \ No newline at end of file +export default TempAdminDashboardPage; diff --git a/client/src/AdminDashboard/UserTable.tsx b/client/src/AdminDashboard/UserTable.tsx index 2e099e85..b0973ae6 100644 --- a/client/src/AdminDashboard/UserTable.tsx +++ b/client/src/AdminDashboard/UserTable.tsx @@ -4,6 +4,7 @@ */ import React, { useEffect, useState } from 'react'; import CircularProgress from '@mui/material/CircularProgress'; +import { LocationCity } from '@material-ui/icons'; import { PaginationTable, TColumn } from '../components/PaginationTable.tsx'; import DeleteUserButton from './DeleteUserButton.tsx'; import PromoteUserButton from './PromoteUserButton.tsx'; @@ -14,8 +15,8 @@ import IUser from '../util/types/user.ts'; interface AdminDashboardRow { key: string; - first: string; - last: string; + city: string; + state: string; email: string; promote: React.ReactElement; remove: React.ReactElement; @@ -28,8 +29,8 @@ interface AdminDashboardRow { function UserTable() { // define columns for the table const columns: TColumn[] = [ - { id: 'first', label: 'First Name' }, - { id: 'last', label: 'Last Name' }, + { id: 'city', label: 'City' }, + { id: 'state', label: 'State' }, { id: 'email', label: 'Email' }, { id: 'promote', label: 'Promote to Admin' }, { id: 'remove', label: 'Remove User' }, @@ -41,11 +42,11 @@ function UserTable() { promote: React.ReactElement, remove: React.ReactElement, ): AdminDashboardRow { - const { _id, firstName, lastName, email } = user; + const { _id, city, state, email } = user; return { key: _id, - first: firstName, - last: lastName, + city, + state, email, promote, remove, diff --git a/client/src/App.tsx b/client/src/App.tsx index 0e9668b5..cbd94476 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -38,7 +38,6 @@ function App() { }> } /> } /> - } /> } @@ -58,6 +57,7 @@ function App() { /> {/* Routes accessed only if user is authenticated */} }> + } /> } /> }> diff --git a/client/src/Authentication/LoginPage.tsx b/client/src/Authentication/LoginPage.tsx index abeb9967..2bad0a3a 100644 --- a/client/src/Authentication/LoginPage.tsx +++ b/client/src/Authentication/LoginPage.tsx @@ -153,11 +153,11 @@ function LoginPage() { const dispatch = useAppDispatch(); function dispatchUser( userEmail: string, - firstName: string, - lastName: string, + city: string, + state: string, admin: boolean, ) { - dispatch(loginRedux({ email: userEmail, firstName, lastName, admin })); + dispatch(loginRedux({ email: userEmail, city, state, admin })); } const clearErrorMessages = () => { @@ -197,12 +197,7 @@ function LoginPage() { loginUser(values.email, values.password) .then((user) => { console.log('navigating to home!'); - dispatchUser( - user.email!, - user.firstName!, - user.lastName!, - user.admin!, - ); + dispatchUser(user.email!, user.city!, user.state!, user.admin!); navigate('/home'); }) .catch((e) => { @@ -270,11 +265,6 @@ function LoginPage() { Forgot password? - - - Sign up - - { setShowError('alert', true); setAlertTitle(''); @@ -158,27 +158,27 @@ function RegisterPage() { setValue('firstName', e.target.value)} + label="City" + value={values.city} + onChange={(e) => setValue('city', e.target.value)} /> setValue('lastName', e.target.value)} + label="State" + value={values.state} + onChange={(e) => setValue('state', e.target.value)} /> diff --git a/client/src/Authentication/api.ts b/client/src/Authentication/api.ts index 7b4502b5..d54cc6b4 100644 --- a/client/src/Authentication/api.ts +++ b/client/src/Authentication/api.ts @@ -38,22 +38,22 @@ async function verifyAccount(verificationToken: string) { /** * Sends a request to the server to register a user for an account - * @param firstName - * @param lastName + * @param city + * @param state * @param email * @param password * @throws An {@link Error} with a `messsage` field describing the issue in verifying */ async function register( - firstName: string, - lastName: string, + city: string, + state: string, email: string, password: string, ) { const lowercaseEmail = email.toLowerCase(); - const res = await postData('auth/register', { - firstName, - lastName, + const res = await postData('admin/register', { + city, + state, email: lowercaseEmail, password, }); @@ -94,8 +94,8 @@ async function resetPassword(password: string, token: string) { /** * Sends a request to the server to register a new user via an invite - * @param firstName - * @param lastName + * @param city + * @param state * @param email * @param password * @param inviteToken @@ -103,16 +103,16 @@ async function resetPassword(password: string, token: string) { * resetting the password */ async function registerInvite( - firstName: string, - lastName: string, + city: string, + state: string, email: string, password: string, inviteToken: string, ) { const lowercaseEmail = email.toLowerCase(); const res = await postData('auth/register-invite', { - firstName, - lastName, + city, + state, email: lowercaseEmail, password, inviteToken, diff --git a/client/src/Home/HomePage.tsx b/client/src/Home/HomePage.tsx index 15638dad..09465d39 100644 --- a/client/src/Home/HomePage.tsx +++ b/client/src/Home/HomePage.tsx @@ -1,7 +1,11 @@ import React, { useState } from 'react'; import Button from '@mui/material/Button'; -import { NavigateFunction, useNavigate } from 'react-router-dom'; -import { Typography, Grid } from '@mui/material'; +import { + NavigateFunction, + useNavigate, + Link as RouterLink, +} from 'react-router-dom'; +import { Link, Typography, Grid } from '@mui/material'; import { useAppDispatch, useAppSelector } from '../util/redux/hooks.ts'; import { logout as logoutAction, @@ -37,12 +41,17 @@ function PromoteButton({ Promote self to admin ) : ( - navigator('/users', { replace: true })} - > - View all users - +
+ navigator('/users', { replace: true })} + > + View all users + + + Register a New Chapter + +
); } /** @@ -69,7 +78,8 @@ function HomePage() { } }; - const message = `Welcome to the Boilerplate, ${user.firstName} ${user.lastName}!`; + const message = `Welcome to the Boilerplate, ${user.city}, ${user.state}!`; + console.log(user); return ( {message} diff --git a/client/src/util/redux/userSlice.ts b/client/src/util/redux/userSlice.ts index 3c2499a5..bec65075 100644 --- a/client/src/util/redux/userSlice.ts +++ b/client/src/util/redux/userSlice.ts @@ -4,22 +4,22 @@ import type { RootState } from './store.ts'; export interface UserState { email: string | null; - firstName: string | null; - lastName: string | null; + city: string | null; + state: string | null; admin: boolean | null; } interface Payload { email: string; - firstName: string; - lastName: string; + city: string; + state: string; admin: boolean; } const initialState = { email: null, - firstName: null, - lastName: null, + city: null, + state: null, } as UserState; /** @@ -31,8 +31,8 @@ const userSlice = createSlice({ reducers: { login: (state, action: PayloadAction) => { state.email = action.payload.email; - state.firstName = action.payload.firstName; - state.lastName = action.payload.lastName; + state.city = action.payload.city; + state.state = action.payload.state; state.admin = action.payload.admin; }, toggleAdmin: (state) => { @@ -40,8 +40,8 @@ const userSlice = createSlice({ }, logout: (state) => { state.email = null; - state.firstName = null; - state.lastName = null; + state.city = null; + state.state = null; state.admin = null; }, }, diff --git a/server/src/config/configPassport.ts b/server/src/config/configPassport.ts index e9fc119d..dc2b1d7a 100644 --- a/server/src/config/configPassport.ts +++ b/server/src/config/configPassport.ts @@ -8,8 +8,10 @@ import { interface IUserWithPassword { _id: string; - firstName: string; - lastName: string; + city: string; + state: string; + isAcceptingRequests: boolean; + isActive: boolean; email: string; password?: string; verified: boolean; diff --git a/server/src/controllers/admin.controller.ts b/server/src/controllers/admin.controller.ts index 53b99643..947fc345 100644 --- a/server/src/controllers/admin.controller.ts +++ b/server/src/controllers/admin.controller.ts @@ -12,6 +12,7 @@ import { getUserByEmail, getAllUsersFromDB, deleteUserById, + createUser, } from '../services/user.service.ts'; import { createInvite, @@ -20,7 +21,11 @@ import { updateInvite, } from '../services/invite.service.ts'; import { IInvite } from '../models/invite.model.ts'; -import { emailInviteLink } from '../services/mail.service.ts'; +import { + emailInviteLink, + emailVerificationLink, +} from '../services/mail.service.ts'; +import mixpanel from '../config/configMixpanel.ts'; /** * Get all users from the database. Upon success, send the a list of all users in the res body with 200 OK status code. @@ -77,6 +82,85 @@ const upgradePrivilege = async ( }); }; +/** + * A controller function to register a user in the database. + */ +const register = async ( + req: express.Request, + res: express.Response, + next: express.NextFunction, +) => { + const { city, state, email, password } = req.body; + if (!city || !(typeof city === 'string')) { + next(ApiError.notFound(`city does not exist or is invalid`)); + return; + } + if (!state || !(typeof state === 'string')) { + next(ApiError.notFound(`state does not exist or is invalid`)); + return; + } + if (!email || !(typeof email === 'string')) { + next(ApiError.notFound(`email does not exist or is invalid`)); + return; + } + if (!password || !(typeof password === 'string')) { + next(ApiError.notFound(`password does not exist or is invalid`)); + return; + } + const emailRegex = + /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/g; + + const passwordRegex = /^[a-zA-Z0-9!?$%^*)(+=._-]{6,61}$/; + + const nameRegex = /^[a-z ,.'-]+/i; + + if ( + !email.match(emailRegex) || + !password.match(passwordRegex) || + !city.match(nameRegex) || + !state.match(nameRegex) + ) { + next(ApiError.badRequest('Invalid email, password, or name.')); + next(ApiError.badRequest('Already logged in.')); + return; + } + const lowercaseEmail = email.toLowerCase(); + // Check if user exists + const existingUser: IUser | null = await getUserByEmail(lowercaseEmail); + if (existingUser) { + next( + ApiError.badRequest( + `An account with email ${lowercaseEmail} already exists.`, + ), + ); + return; + } + + // Create user and send verification email + try { + const user = await createUser(city, state, lowercaseEmail, password); + // Don't need verification email if testing + if (process.env.NODE_ENV === 'test') { + user!.verified = true; + await user?.save(); + } else { + const verificationToken = crypto.randomBytes(32).toString('hex'); + user!.verificationToken = verificationToken; + await user!.save(); + await emailVerificationLink(lowercaseEmail, verificationToken); + } + // Mixpanel Register tracking + mixpanel.track('Register', { + distinct_id: user?._id, + email: user?.email, + }); + + res.sendStatus(StatusCode.CREATED); + } catch (err) { + next(ApiError.internal('Unable to register user.')); + } +}; + /** * Delete a user from the database. The email of the user is expected to be in the request parameter (url). Send a 200 OK status code on success. */ @@ -234,4 +318,11 @@ const inviteUser = async ( } }; -export { getAllUsers, upgradePrivilege, deleteUser, verifyToken, inviteUser }; +export { + getAllUsers, + upgradePrivilege, + register, + deleteUser, + verifyToken, + inviteUser, +}; diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index d6062590..711c915a 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -11,21 +11,12 @@ import { IUser } from '../models/user.model.ts'; import StatusCode from '../util/statusCode.ts'; import { passwordHashSaltRounds, - createUser, getUserByEmail, getUserByResetPasswordToken, getUserByVerificationToken, } from '../services/user.service.ts'; -import { - emailResetPasswordLink, - emailVerificationLink, -} from '../services/mail.service.ts'; +import { emailResetPasswordLink } from '../services/mail.service.ts'; import ApiError from '../util/apiError.ts'; -import { - getInviteByToken, - removeInviteByToken, -} from '../services/invite.service.ts'; -import { IInvite } from '../models/invite.model.ts'; import mixpanel from '../config/configMixpanel.ts'; /** @@ -118,84 +109,6 @@ const logout = async ( }); }; -/** - * A controller function to register a user in the database. - */ -const register = async ( - req: express.Request, - res: express.Response, - next: express.NextFunction, -) => { - const { firstName, lastName, email, password } = req.body; - if (!firstName || !lastName || !email || !password) { - next( - ApiError.missingFields(['firstName', 'lastName', 'email', 'password']), - ); - return; - } - const emailRegex = - /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/g; - - const passwordRegex = /^[a-zA-Z0-9!?$%^*)(+=._-]{6,61}$/; - - const nameRegex = /^[a-z ,.'-]+/i; - - if ( - !email.match(emailRegex) || - !password.match(passwordRegex) || - !firstName.match(nameRegex) || - !lastName.match(nameRegex) - ) { - next(ApiError.badRequest('Invalid email, password, or name.')); - return; - } - - if (req.isAuthenticated()) { - next(ApiError.badRequest('Already logged in.')); - return; - } - const lowercaseEmail = email.toLowerCase(); - // Check if user exists - const existingUser: IUser | null = await getUserByEmail(lowercaseEmail); - if (existingUser) { - next( - ApiError.badRequest( - `An account with email ${lowercaseEmail} already exists.`, - ), - ); - return; - } - - // Create user and send verification email - try { - const user = await createUser( - firstName, - lastName, - lowercaseEmail, - password, - ); - // Don't need verification email if testing - if (process.env.NODE_ENV === 'test') { - user!.verified = true; - await user?.save(); - } else { - const verificationToken = crypto.randomBytes(32).toString('hex'); - user!.verificationToken = verificationToken; - await user!.save(); - await emailVerificationLink(lowercaseEmail, verificationToken); - } - // Mixpanel Register tracking - mixpanel.track('Register', { - distinct_id: user?._id, - email: user?.email, - }); - - res.sendStatus(StatusCode.CREATED); - } catch (err) { - next(ApiError.internal('Unable to register user.')); - } -}; - /** * A dummy controller function which sends a 200 OK status code. Should be used to close a request after a middleware call. */ @@ -325,89 +238,11 @@ const resetPassword = async ( } }; -const registerInvite = async ( - req: express.Request, - res: express.Response, - next: express.NextFunction, -) => { - const { firstName, lastName, email, password, inviteToken } = req.body; - if (!firstName || !lastName || !email || !password) { - next( - ApiError.missingFields([ - 'firstName', - 'lastName', - 'email', - 'password', - 'inviteToken', - ]), - ); - return; - } - const emailRegex = - /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/g; - - const passwordRegex = /^[a-zA-Z0-9!?$%^*)(+=._-]{6,61}$/; - - const nameRegex = /^[a-z ,.'-]+/i; - - if ( - !email.match(emailRegex) || - !password.match(passwordRegex) || - !firstName.match(nameRegex) || - !lastName.match(nameRegex) - ) { - next(ApiError.badRequest('Invalid email, password, or name.')); - return; - } - - if (req.isAuthenticated()) { - next(ApiError.badRequest('Already logged in.')); - return; - } - - // Check if invite exists - const invite: IInvite | null = await getInviteByToken(inviteToken); - if (!invite || invite.email !== email) { - next(ApiError.badRequest(`Invalid invite`)); - return; - } - - const lowercaseEmail = email.toLowerCase(); - // Check if user exists - const existingUser: IUser | null = await getUserByEmail(lowercaseEmail); - if (existingUser) { - next( - ApiError.badRequest( - `An account with email ${lowercaseEmail} already exists.`, - ), - ); - return; - } - - // Create user - try { - const user = await createUser( - firstName, - lastName, - lowercaseEmail, - password, - ); - user!.verified = true; - await user?.save(); - await removeInviteByToken(inviteToken); - res.sendStatus(StatusCode.CREATED); - } catch (err) { - next(ApiError.internal('Unable to register user.')); - } -}; - export { login, logout, - register, approve, verifyAccount, sendResetPasswordEmail, resetPassword, - registerInvite, }; diff --git a/server/src/controllers/birthdayRequest.controller.ts b/server/src/controllers/birthdayRequest.controller.ts index 9a323d1c..b338f38c 100644 --- a/server/src/controllers/birthdayRequest.controller.ts +++ b/server/src/controllers/birthdayRequest.controller.ts @@ -11,7 +11,7 @@ import { deleteRequestByID, createBirthdayRequestByID, } from '../services/birthdayRequest.service.ts'; -import { getChapterById } from '../services/chapter.service.ts'; +import { getUserById } from '../services/user.service.ts'; import { emailRequestUpdate, emailRequestDelete, @@ -22,7 +22,7 @@ import { ChildSituation, IBirthdayRequest, } from '../models/birthdayRequest.model.ts'; -import { IChapter } from '../models/chapter.model.ts'; +import { IUser } from '../models/user.model.ts'; const getAllRequests = async ( req: express.Request, @@ -74,7 +74,7 @@ const updateRequestStatus = async ( return; } // get chapter email by chapter ID - const chapter: IChapter | null = await getChapterById(request.chapterId); + const chapter: IUser | null = await getUserById(request.chapterId); if (!chapter) { next(ApiError.notFound(`Chapter does not exist`)); return; @@ -128,7 +128,7 @@ const deleteRequest = async ( return; } // get chapter email by chapter ID - const chapter: IChapter | null = await getChapterById(request.chapterId); + const chapter: IUser | null = await getUserById(request.chapterId); if (!chapter) { next(ApiError.notFound(`Chapter does not exist`)); return; diff --git a/server/src/controllers/chapter.controller.ts b/server/src/controllers/chapter.controller.ts deleted file mode 100644 index 6d93144a..00000000 --- a/server/src/controllers/chapter.controller.ts +++ /dev/null @@ -1,198 +0,0 @@ -/* eslint-disable import/prefer-default-export */ -import express from 'express'; -import ApiError from '../util/apiError.ts'; -import StatusCode from '../util/statusCode.ts'; -import { - toggleRequestByID, - getAllChaptersFromDB, - getChapterById, - deleteChapterByID, - createChapterByID, -} from '../services/chapter.service.ts'; -import { IChapter } from '../models/chapter.model.ts'; - -const getAllChapters = async ( - req: express.Request, - res: express.Response, - next: express.NextFunction, -) => { - return ( - getAllChaptersFromDB() - .then((userList) => { - res.status(StatusCode.OK).send(userList); - }) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .catch((e) => { - next(ApiError.internal('Unable to retrieve all users')); - }) - ); -}; - -const toggleRequest = async ( - req: express.Request, - res: express.Response, - next: express.NextFunction, -) => { - const { id } = req.params; - if (!id) { - next(ApiError.missingFields(['id'])); - return; - } - - toggleRequestByID(id) - .then(() => { - res.sendStatus(StatusCode.OK); - }) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .catch((e) => { - next(ApiError.internal('Unable to toggle status.')); - }); -}; - -const deleteChapter = async ( - req: express.Request, - res: express.Response, - next: express.NextFunction, -) => { - // request id - const { id } = req.params; - if (!id) { - next(ApiError.missingFields(['id'])); - return; - } - // get chapter by chapter ID - const chapter: IChapter | null = await getChapterById(id); - if (!chapter) { - next(ApiError.notFound(`Chapter does not exist`)); - return; - } - - deleteChapterByID(id) - .then(() => res.sendStatus(StatusCode.OK)) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .catch((e) => { - next(ApiError.internal('Failed to delete chapter.')); - }); -}; - -const createChapter = async ( - req: express.Request, - res: express.Response, - next: express.NextFunction, -) => { - const { - city, - state, - isAcceptingRequests, - email, - password, - verified, - verificationToken, - resetPasswordToken, - resetPasswordTokenExpiryDate, - isAdmin, - } = req.body; - if (!city || !(typeof city === 'string')) { - next(ApiError.notFound(`city does not exist or is invalid`)); - return; - } - if (!state || !(typeof state === 'string')) { - next(ApiError.notFound(`state does not exist or is invalid`)); - return; - } - if (isAcceptingRequests !== true && isAcceptingRequests !== false) { - next(ApiError.notFound(`isAcceptingRequests does not exist or is invalid`)); - return; - } - if (!email || !(typeof email === 'string')) { - next(ApiError.notFound(`email does not exist or is invalid`)); - return; - } - if (!password || !(typeof password === 'string')) { - next(ApiError.notFound(`password does not exist or is invalid`)); - return; - } - if (verified !== true && verified !== false) { - next(ApiError.notFound(`verified does not exist or is invalid`)); - return; - } - // not sure if this is right, it was chat gpt'ed lol - if ( - verificationToken !== undefined && - verificationToken !== null && - typeof verificationToken !== 'string' - ) { - next( - ApiError.notFound( - `verificationToken must be a string, null, or undefined`, - ), - ); - return; - } - - if ( - resetPasswordToken !== undefined && - resetPasswordToken !== null && - typeof resetPasswordToken !== 'string' - ) { - next( - ApiError.notFound( - `resetPasswordToken must be a string, null, or undefined`, - ), - ); - return; - } - - if ( - resetPasswordTokenExpiryDate !== undefined && - resetPasswordTokenExpiryDate !== null - ) { - try { - // Convert the string to a Date object - req.body.resetPasswordTokenExpiryDate = new Date( - resetPasswordTokenExpiryDate, - ); - } catch (e) { - next( - ApiError.notFound( - `resetPasswordTokenExpiryDate must be a valid date string, null, or undefined`, - ), - ); - return; - } - } - if (isAdmin !== true && isAdmin !== false) { - next(ApiError.notFound(`isAdmin does not exist or is invalid`)); - return; - } - try { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const user = await createChapterByID( - city, - state, - isAcceptingRequests, - email, - password, - verified, - verificationToken, - resetPasswordToken, - resetPasswordTokenExpiryDate, - isAdmin, - ); - res.sendStatus(StatusCode.CREATED); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (err: any) { - // Add better error handling - // console.error('Chapter creation error:', err); - if (err.code === 11000) { - // MongoDB duplicate key error - next( - ApiError.badRequest('A chapter with this city or email already exists'), - ); - } else { - next(ApiError.internal(`Unable to register chapter: ${err.message}`)); - } - } -}; - -export { toggleRequest, getAllChapters, deleteChapter, createChapter }; diff --git a/server/src/controllers/user.controller.ts b/server/src/controllers/user.controller.ts new file mode 100644 index 00000000..0e02d01b --- /dev/null +++ b/server/src/controllers/user.controller.ts @@ -0,0 +1,28 @@ +/* eslint-disable import/prefer-default-export */ +import express from 'express'; +import ApiError from '../util/apiError.ts'; +import StatusCode from '../util/statusCode.ts'; +import { toggleRequestByID } from '../services/user.service.ts'; + +const toggleRequest = async ( + req: express.Request, + res: express.Response, + next: express.NextFunction, +) => { + const { id } = req.params; + if (!id) { + next(ApiError.missingFields(['id'])); + return; + } + + toggleRequestByID(id) + .then(() => { + res.sendStatus(StatusCode.OK); + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .catch((e) => { + next(ApiError.internal('Unable to toggle status.')); + }); +}; + +export { toggleRequest }; diff --git a/server/src/models/chapter.model.ts b/server/src/models/chapter.model.ts deleted file mode 100644 index 29336a76..00000000 --- a/server/src/models/chapter.model.ts +++ /dev/null @@ -1,73 +0,0 @@ -import mongoose from 'mongoose'; - -const ChapterSchema = new mongoose.Schema({ - city: { - type: String, - required: true, - unique: true, - }, - state: { - type: String, - required: true, - }, - isAcceptingRequests: { - type: Boolean, - required: true, - default: true, - }, - email: { - type: String, - required: true, - unique: true, - match: - /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/, - }, - password: { - type: String, - required: true, - }, - verified: { - type: Boolean, - required: true, - default: false, - }, - verificationToken: { - type: String, - required: false, - unique: true, - sparse: true, - }, - resetPasswordToken: { - type: String, - required: false, - unique: true, - sparse: true, - }, - resetPasswordTokenExpiryDate: { - type: Date, - required: false, - }, - isAdmin: { - type: Boolean, - required: true, - default: false, - }, -}); - -interface IChapter extends mongoose.Document { - _id: string; - city: string; - state: string; - isAcceptingRequests: boolean; - email: string; - password: string; - verified: boolean; - verificationToken: string | null | undefined; - resetPasswordToken: string | null | undefined; - resetPasswordTokenExpiryDate: Date | null | undefined; - isAdmin: boolean; -} - -const Chapter = mongoose.model('Chapter', ChapterSchema); - -export { IChapter, Chapter }; diff --git a/server/src/models/user.model.ts b/server/src/models/user.model.ts index 1219a2e1..0375bdbe 100644 --- a/server/src/models/user.model.ts +++ b/server/src/models/user.model.ts @@ -5,14 +5,24 @@ import mongoose from 'mongoose'; const UserSchema = new mongoose.Schema({ - firstName: { + city: { type: String, required: true, }, - lastName: { + state: { type: String, required: true, }, + isAcceptingRequests: { + type: Boolean, + required: true, + default: true, + }, + isActive: { + type: Boolean, + required: true, + default: true, + }, email: { type: String, match: @@ -52,8 +62,10 @@ const UserSchema = new mongoose.Schema({ interface IUser extends mongoose.Document { _id: string; - firstName: string; - lastName: string; + city: string; + state: string; + isAcceptingRequests: boolean; + isActive: boolean; email: string; password: string; verified: boolean; diff --git a/server/src/routes/__tests__/auth.test.ts b/server/src/routes/__tests__/auth.test.ts index f754f8fa..699acd08 100644 --- a/server/src/routes/__tests__/auth.test.ts +++ b/server/src/routes/__tests__/auth.test.ts @@ -58,8 +58,8 @@ describe('testing authentication routes', () => { const user = await User.findOne({ email: testEmail }); expect(user).toBeTruthy(); expect(user?.email).toBe(testEmail); - expect(user?.firstName).toBe(testFirstName); - expect(user?.lastName).toBe(testLastName); + expect(user?.city).toBe(testFirstName); + expect(user?.state).toBe(testLastName); }); }); diff --git a/server/src/routes/admin.route.ts b/server/src/routes/admin.route.ts index 7f18c255..540c7926 100644 --- a/server/src/routes/admin.route.ts +++ b/server/src/routes/admin.route.ts @@ -10,6 +10,7 @@ import { deleteUser, inviteUser, verifyToken, + register, } from '../controllers/admin.controller.ts'; import { isAuthenticated } from '../controllers/auth.middleware.ts'; import { approve } from '../controllers/auth.controller.ts'; @@ -65,4 +66,13 @@ router.post('/invite', isAuthenticated, isAdmin, inviteUser); */ router.get('/invite/:token', verifyToken); +/** + * A POST route to register a user (can only be done by admin). Expects a JSON body with the following fields: + * - city (string) - The first name of the user + * - state (string) - The last name of the user + * - email (string) - The email of the user + * - password (string) - The password of the user + */ +router.post('/register', isAuthenticated, isAdmin, register); + export default router; diff --git a/server/src/routes/auth.route.ts b/server/src/routes/auth.route.ts index 778a5d76..d44d9719 100644 --- a/server/src/routes/auth.route.ts +++ b/server/src/routes/auth.route.ts @@ -6,27 +6,16 @@ import express from 'express'; import { login, logout, - register, approve, sendResetPasswordEmail, resetPassword, verifyAccount, - registerInvite, } from '../controllers/auth.controller.ts'; import { isAuthenticated } from '../controllers/auth.middleware.ts'; import 'dotenv/config'; const router = express.Router(); -/** - * A POST route to register a user. Expects a JSON body with the following fields: - * - firstName (string) - The first name of the user - * - lastName (string) - The last name of the user - * - email (string) - The email of the user - * - password (string) - The password of the user - */ -router.post('/register', register); - /** * A POST route to verify a user's account. Expects a JSON body with the following fields: * - token (string) - The token identifying the verification attempt @@ -67,10 +56,4 @@ router.post('/reset-password', resetPassword); */ router.get('/authstatus', isAuthenticated, approve); -/** - * A POST register a user from an invite. If the information and invite are valid - * a new account is created. Otherwise a 400 bad request error is returned - */ -router.post('/register-invite', registerInvite); - export default router; diff --git a/server/src/routes/routers.ts b/server/src/routes/routers.ts index fb5d6ed3..90b99d1f 100644 --- a/server/src/routes/routers.ts +++ b/server/src/routes/routers.ts @@ -9,7 +9,7 @@ The prefix should be of the form '/api/ROUTERNAME' import { Router } from 'express'; import adminRouter from './admin.route.ts'; import authRouter from './auth.route.ts'; -import chapterRouter from './chapter.route.ts'; +import userRouter from './user.route.ts'; import birthdayRequestRouter from './birthdayRequest.route.ts'; const prefixToRouterMap: { prefix: string; router: Router }[] = [ @@ -22,8 +22,8 @@ const prefixToRouterMap: { prefix: string; router: Router }[] = [ router: adminRouter, }, { - prefix: '/api/chapter', - router: chapterRouter, + prefix: '/api/user', + router: userRouter, }, { prefix: '/api/birthdayrequest', diff --git a/server/src/routes/chapter.route.ts b/server/src/routes/user.route.ts similarity index 53% rename from server/src/routes/chapter.route.ts rename to server/src/routes/user.route.ts index af6a4d19..56e1e3aa 100644 --- a/server/src/routes/chapter.route.ts +++ b/server/src/routes/user.route.ts @@ -1,12 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import express from 'express'; import { isAdmin } from '../controllers/admin.middleware.ts'; -import { - toggleRequest, - getAllChapters, - createChapter, - deleteChapter, -} from '../controllers/chapter.controller.ts'; +import { toggleRequest } from '../controllers/user.controller.ts'; import { isAuthenticated } from '../controllers/auth.middleware.ts'; import 'dotenv/config'; @@ -14,10 +9,4 @@ const router = express.Router(); router.put('/toggleRequests/:id', isAuthenticated, isAdmin, toggleRequest); -router.get('/all', isAuthenticated, isAdmin, getAllChapters); - -router.post('/create/', isAuthenticated, isAdmin, createChapter); - -router.delete('/delete/:id', isAuthenticated, isAdmin, deleteChapter); - export default router; diff --git a/server/src/services/chapter.service.ts b/server/src/services/chapter.service.ts deleted file mode 100644 index f2cf7352..00000000 --- a/server/src/services/chapter.service.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* eslint-disable import/prefer-default-export */ -import { Chapter } from '../models/chapter.model.ts'; - -const removeSensitiveDataQuery = [ - '-password', - '-verificationToken', - '-resetPasswordToken', - '-resetPasswordTokenExpiryDate', -]; - -const toggleRequestByID = async (id: string) => { - const chapter = await Chapter.findByIdAndUpdate(id, [ - { $set: { isAcceptingRequests: { $not: '$isAcceptingRequests' } } }, - ]).exec(); - return chapter; -}; - -const getAllChaptersFromDB = async () => { - const userList = await Chapter.find({}) - .select(removeSensitiveDataQuery) - .exec(); - return userList; -}; - -const getChapterById = async (id: string) => { - const chapter = await Chapter.findById(id) - .select(removeSensitiveDataQuery) - .exec(); - return chapter; -}; - -const deleteChapterByID = async (id: string) => { - const chapter = await Chapter.findByIdAndDelete(id).exec(); - return chapter; -}; - -/** - * Creates a new chapter in the database. - * @param city - The city the chapter is located in - * @param state - The state the chapter is located in - * @param isAcceptingRequests - Whether the chapter is accepting requests - * @param email - The email of the chapter - * @param password - The password of the chapter - * @param verified - Whether the chapter is verified - * @param verificationToken - The verification token of the chapter - * @param resetPasswordToken - The reset password token of the chapter - * @param resetPasswordTokenExpiryDate - The expiry date of the reset password token - * @param isAdmin - Whether the chapter is an admin - * @returns The created {@link Chapter} - */ -const createChapterByID = async ( - city: string, - state: string, - isAcceptingRequests: boolean, - email: string, - password: string, - verified: boolean, - verificationToken: string, - resetPasswordToken: string, - resetPasswordTokenExpiryDate: Date, - isAdmin: boolean, -) => { - const newChapter = new Chapter({ - city, - state, - isAcceptingRequests, - email, - password, - verified, - verificationToken, - resetPasswordToken, - resetPasswordTokenExpiryDate, - isAdmin, - }); - const returnedChapter = await newChapter.save(); - return returnedChapter; -}; - -export { - toggleRequestByID, - getAllChaptersFromDB, - getChapterById, - deleteChapterByID, - createChapterByID, -}; diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index ce82fe8b..2eb5d954 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -20,15 +20,15 @@ const removeSensitiveDataQueryKeepPassword = [ /** * Creates a new user in the database. - * @param firstName - string representing the first name of the user - * @param lastName - string representing the last name of the user + * @param city - string representing the city of the chapter + * @param state - string representing the state of the chapter * @param email - string representing the email of the user * @param password - string representing the password of the user * @returns The created {@link User} */ const createUser = async ( - firstName: string, - lastName: string, + city: string, + state: string, email: string, password: string, ) => { @@ -37,8 +37,8 @@ const createUser = async ( return null; } const newUser = new User({ - firstName, - lastName, + city, + state, email, password: hashedPassword, admin: false, @@ -141,6 +141,13 @@ const deleteUserById = async (id: string) => { return user; }; +const toggleRequestByID = async (id: string) => { + const chapter = await User.findByIdAndUpdate(id, [ + { $set: { isAcceptingRequests: { $not: '$isAcceptingRequests' } } }, + ]).exec(); + return chapter; +}; + export { passwordHashSaltRounds, createUser, @@ -152,4 +159,5 @@ export { getAllUsersFromDB, upgradeUserToAdmin, deleteUserById, + toggleRequestByID, };