diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be27748..c9a473a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: push: - branches: [ main ] + branches: [main] pull_request: jobs: @@ -40,4 +40,4 @@ jobs: run: npm ci - name: Build frontend & backend - run: npx nx build backend frontend \ No newline at end of file + run: npx nx build backend frontend diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d2dd0a6..9127048 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -9,13 +9,13 @@ # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # -name: "CodeQL Advanced" +name: 'CodeQL Advanced' on: push: - branches: [ "main" ] + branches: ['main'] pull_request: - branches: [ "main" ] + branches: ['main'] schedule: - cron: '43 6 * * 6' @@ -43,10 +43,10 @@ jobs: fail-fast: false matrix: include: - - language: actions - build-mode: none - - language: javascript-typescript - build-mode: none + - language: actions + build-mode: none + - language: javascript-typescript + build-mode: none # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both @@ -56,45 +56,45 @@ jobs: # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - # Add any setup steps before running the `github/codeql-action/init` action. - # This includes steps like installing compilers or runtimes (`actions/setup-node` - # or others). This is typically only required for manual builds. - # - name: Setup runtime (example) - # uses: actions/setup-example@v1 + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality - # If the analyze step fails for one of the languages you are analyzing with - # "We were unable to automatically build your code", modify the matrix above - # to set the build mode to "manual" for that language. Then modify this step - # to build your code. - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: '/language:${{matrix.language}}' diff --git a/.prettierrc b/.prettierrc index 33645d2..534a38a 100644 --- a/.prettierrc +++ b/.prettierrc @@ -5,4 +5,4 @@ "printWidth": 100, "trailingComma": "none", "arrowParens": "avoid" -} \ No newline at end of file +} diff --git a/README.md b/README.md index 7504ce6..8b9ad96 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ npm run dev Deploy the application swiftly using Docker Compose. 🚀 Ensure Docker is running. -> [!IMPORTANT] +> [!IMPORTANT] > Before launching, configure your environment variables. The backend service needs an `apps/backend/.env.local` file (copy `apps/backend/.env` if needed). The `JWT_SECRET` is crucial. ✨ Run this from the project root: @@ -76,6 +76,7 @@ docker-compose up -d This builds and starts frontend and backend services in detached mode. Access: + - **Frontend**: `http://localhost:4200` đŸ–Ĩī¸ - **Backend API**: `http://localhost:3333` âš™ī¸ @@ -112,5 +113,4 @@ mtes/ This project is licensed under the GPL-3.0 License. It utilizes a similar tech stack and codebase inspired by [FIRSTIsrael/lems](https://github.com/FIRSTIsrael/lems); 🙏 thank you for making this possible! 🚀 - -***Made with â¤ī¸ by [@TheCommandCat](https://github.com/TheCommandCat)*** +**_Made with â¤ī¸ by [@TheCommandCat](https://github.com/TheCommandCat)_** diff --git a/apps/backend/src/lib/schedule/cleaner.ts b/apps/backend/src/lib/schedule/cleaner.ts index 9ae31cb..37e32bd 100644 --- a/apps/backend/src/lib/schedule/cleaner.ts +++ b/apps/backend/src/lib/schedule/cleaner.ts @@ -1,18 +1,54 @@ import * as db from '@mtes/database'; +import { ObjectId } from 'mongodb'; -export const cleanDivisionData = async () => { - if (!(await db.deleteElectionEvent())) throw new Error('Could not delete event!'); - if (!(await db.deleteUsers( - { isAdmin: { $ne: true } } - )).acknowledged) throw new Error('Could not delete users!'); - if (!(await db.deleteElectionState()).acknowledged) { +export const cleanDivisionData = async (eventId: ObjectId) => { + if (!(await db.deleteElectionEvent(eventId))) throw new Error('Could not delete event!'); + if (!(await db.deleteUsers({ _id: eventId })).acknowledged) + throw new Error('Could not delete users!'); + if (!(await db.deleteElectionState(eventId)).acknowledged) { throw new Error('Could not delete Election state!'); } - if (!(await db.deleteMembers({})).acknowledged) throw new Error('Could not delete members!'); - if (!(await db.deleteContestants()).acknowledged) throw new Error('Could not delete contestant!'); - if (!(await db.deleteRounds()).acknowledged) throw new Error('Could not delete rounds!'); - if (!(await db.deleteVotes()).acknowledged) throw new Error('Could not delete votes!'); - if (!(await db.deleteVotingStatuses()).acknowledged) throw new Error('Could not delete voting statuses!'); - if (!(await db.deleteCities()).acknowledged) throw new Error('Could not delete cities!'); + if ( + !( + await db.deleteMembers({ + _id: eventId + }) + ).acknowledged + ) + throw new Error('Could not delete members!'); + if ( + !( + await db.deleteContestants({ + _id: eventId + }) + ).acknowledged + ) + throw new Error('Could not delete contestant!'); + if (!(await db.deleteRounds({ eventId })).acknowledged) + throw new Error('Could not delete rounds!'); + if ( + !( + await db.deleteVotes({ + _id: eventId + }) + ).acknowledged + ) + throw new Error('Could not delete votes!'); + if ( + !( + await db.deleteVotingStatuses({ + _id: eventId + }) + ).acknowledged + ) + throw new Error('Could not delete voting statuses!'); + if ( + !( + await db.deleteCities({ + _id: eventId + }) + ).acknowledged + ) + throw new Error('Could not delete cities!'); }; diff --git a/apps/backend/src/lib/schedule/voting-stands-users.ts b/apps/backend/src/lib/schedule/voting-stands-users.ts index 11b0cfa..9e89526 100644 --- a/apps/backend/src/lib/schedule/voting-stands-users.ts +++ b/apps/backend/src/lib/schedule/voting-stands-users.ts @@ -1,4 +1,5 @@ import { User } from '@mtes/types'; +import { ObjectId } from 'mongodb'; const randomString = (length: number) => { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; @@ -9,10 +10,11 @@ const randomString = (length: number) => { return result; }; -export const CreateVotingStandUsers = (numOfStands: number): User[] => { - const users = []; +export const CreateVotingStandUsers = (numOfStands: number, eventId: ObjectId): User[] => { + const users: User[] = []; users.push({ + eventId: eventId, isAdmin: false, role: 'election-manager', password: randomString(4), @@ -21,6 +23,7 @@ export const CreateVotingStandUsers = (numOfStands: number): User[] => { for (let i = 1; i <= numOfStands; i++) { users.push({ + eventId: eventId, isAdmin: false, role: 'voting-stand', password: randomString(4), @@ -33,12 +36,12 @@ export const CreateVotingStandUsers = (numOfStands: number): User[] => { } users.push({ + eventId: eventId, isAdmin: false, role: 'audience-display', password: randomString(4), - lastPasswordSetDate: new Date(), + lastPasswordSetDate: new Date() }); - return users; }; diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index f730cbd..6a78832 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -15,10 +15,7 @@ const port = process.env.PORT ? Number(process.env.PORT) : 3333; const app = express(); const server = http.createServer(app); const corsOptions = { - origin: [ - /localhost:\d+$/, - /\.thecommandcat\.me$/ - ], + origin: [/localhost:\d+$/, /\.thecommandcat\.me$/], credentials: true }; const io = new Server(server, { cors: corsOptions }); @@ -51,4 +48,4 @@ server.listen(port, () => { console.log(`✅ Server started on port ${port}.`); }); -server.on('error', console.error); \ No newline at end of file +server.on('error', console.error); diff --git a/apps/backend/src/middlewares/auth.ts b/apps/backend/src/middlewares/auth.ts index b1f249e..9b57df4 100644 --- a/apps/backend/src/middlewares/auth.ts +++ b/apps/backend/src/middlewares/auth.ts @@ -18,7 +18,7 @@ export const authMiddleware = async (req: Request, res: Response, next: NextFunc if (user) { delete user.password; req.user = user; - return next(); + return next(); } } catch (err) { //Invalid token diff --git a/apps/backend/src/routers/api/admin/events/cities.ts b/apps/backend/src/routers/api/admin/events/cities.ts index ba017ea..199c757 100644 --- a/apps/backend/src/routers/api/admin/events/cities.ts +++ b/apps/backend/src/routers/api/admin/events/cities.ts @@ -5,98 +5,100 @@ import { City } from '@mtes/types'; const router = express.Router({ mergeParams: true }); -router.get('/', asyncHandler(async (_req: Request, res: Response) => { +router.get( + '/', + asyncHandler(async (_req: Request, res: Response) => { console.log('âŦ Getting cities...'); const cities = await db.getCities({}); res.json(cities); -} -)); + }) +); router.post( - '/', - asyncHandler(async (req: Request, res: Response) => { - const cityData: City = req.body; - - if (!cityData?.name || cityData?.numOfVoters === undefined) { - res.status(400).json({ error: 'Missing required fields: name and numOfVoters' }); - return; - } - - if (typeof cityData.numOfVoters !== 'number' || cityData.numOfVoters < 0) { - res.status(400).json({ error: 'Invalid numOfVoters: must be a non-negative number' }); - return; - } - - try { - const result = await db.addCity(cityData); - if (result.insertedId) { - res.status(201).json({ message: 'City added successfully', cityId: result.insertedId }); - } else { - res.status(500).json({ error: 'Failed to add city, no ID returned' }); - } - } catch (error) { - console.error('Error adding city:', error); - if (error.code === 11000) { - res.status(409).json({ error: 'City with this name already exists' }); - return; - } - res.status(500).json({ error: 'Failed to add city due to an internal error' }); - } - }) + '/', + asyncHandler(async (req: Request, res: Response) => { + const cityData: City = req.body; + + if (!cityData?.name || cityData?.numOfVoters === undefined) { + res.status(400).json({ error: 'Missing required fields: name and numOfVoters' }); + return; + } + + if (typeof cityData.numOfVoters !== 'number' || cityData.numOfVoters < 0) { + res.status(400).json({ error: 'Invalid numOfVoters: must be a non-negative number' }); + return; + } + + try { + const result = await db.addCity(cityData); + if (result.insertedId) { + res.status(201).json({ message: 'City added successfully', cityId: result.insertedId }); + } else { + res.status(500).json({ error: 'Failed to add city, no ID returned' }); + } + } catch (error) { + console.error('Error adding city:', error); + if (error.code === 11000) { + res.status(409).json({ error: 'City with this name already exists' }); + return; + } + res.status(500).json({ error: 'Failed to add city due to an internal error' }); + } + }) ); router.put( - '/', - asyncHandler(async (req: Request, res: Response) => { - const { cities } = req.body as { cities: City[] }; - - if (!cities || !Array.isArray(cities)) { - console.log('❌ Cities array is missing or not an array'); - res.status(400).json({ ok: false, message: 'Cities data is missing or invalid' }); - return; - } - - if (cities.length === 0) { - console.log('❌ Cities array is empty'); - } - - console.log('âŦ Updating Cities...'); - - const processedCities = cities.map(city => { - const numVoters = Number(city.numOfVoters); - return { - ...city, - numOfVoters: numVoters - }; - }); - - for (const city of processedCities) { - if (isNaN(city.numOfVoters)) { - console.warn(`âš ī¸ numOfVoters for city '${city.name}' was NaN after conversion.`); - } - } - - const deleteRes = await db.deleteCities(); - if (!deleteRes.acknowledged) { - console.log('❌ Could not delete cities'); - res.status(500).json({ ok: false, message: 'Could not delete cities' }); - return; - } - - if (processedCities.length > 0) { - const addRes = await db.addCities(processedCities.map(city => ({ ...city, _id: undefined }))); - if (!addRes.acknowledged) { - console.log('❌ Could not add cities'); - res.status(500).json({ ok: false, message: 'Could not add cities' }); - return; - } - } else { - console.log('â„šī¸ No cities to add after processing (or initial array was empty).'); - } - - console.log('✅ Cities updated!'); - res.json({ ok: true }); - }) + '/', + asyncHandler(async (req: Request, res: Response) => { + const { cities } = req.body as { cities: City[] }; + + if (!cities || !Array.isArray(cities)) { + console.log('❌ Cities array is missing or not an array'); + res.status(400).json({ ok: false, message: 'Cities data is missing or invalid' }); + return; + } + + if (cities.length === 0) { + console.log('❌ Cities array is empty'); + } + + console.log('âŦ Updating Cities...'); + + const processedCities = cities.map(city => { + const numVoters = Number(city.numOfVoters); + return { + ...city, + numOfVoters: numVoters + }; + }); + + for (const city of processedCities) { + if (isNaN(city.numOfVoters)) { + console.warn(`âš ī¸ numOfVoters for city '${city.name}' was NaN after conversion.`); + } + } + + const deleteRes = await db.deleteCities({}); + if (!deleteRes.acknowledged) { + console.log('❌ Could not delete cities'); + res.status(500).json({ ok: false, message: 'Could not delete cities' }); + return; + } + + if (processedCities.length > 0) { + const addRes = await db.addCities(processedCities.map(city => ({ ...city, _id: undefined }))); + if (!addRes.acknowledged) { + console.log('❌ Could not add cities'); + res.status(500).json({ ok: false, message: 'Could not add cities' }); + return; + } + } else { + console.log('â„šī¸ No cities to add after processing (or initial array was empty).'); + } + + console.log('✅ Cities updated!'); + res.json({ ok: true }); + }) ); export default router; diff --git a/apps/backend/src/routers/api/admin/events/index.ts b/apps/backend/src/routers/api/admin/events/index.ts index de573de..2d4be62 100644 --- a/apps/backend/src/routers/api/admin/events/index.ts +++ b/apps/backend/src/routers/api/admin/events/index.ts @@ -6,6 +6,7 @@ import { ElectionEvent, ElectionState, User, Member } from '@mtes/types'; // Add import * as db from '@mtes/database'; import { cleanDivisionData } from 'apps/backend/src/lib/schedule/cleaner'; import { CreateVotingStandUsers } from 'apps/backend/src/lib/schedule/voting-stands-users'; +import { ObjectId } from 'mongodb'; const randomString = (length: number) => { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; @@ -18,13 +19,14 @@ const randomString = (length: number) => { const router = express.Router({ mergeParams: true }); -function getInitialDivisionState(): ElectionState { +function getInitialDivisionState(eventId: ObjectId): ElectionState { return { + eventId: eventId, activeRound: null, completed: false, audienceDisplay: { - display: 'round', - }, + display: 'round' + } }; } @@ -35,18 +37,29 @@ router.post( // Validate required fields if (!eventData?.name || !eventData?.votingStands) { - res.status(400).json({ error: '׊ם האירו×ĸ ומספר ×ĸמדו×Ē ×”×Ļב×ĸה הם שדו×Ē ×—×•×‘×”' }); + if (!eventData.name) { + res.status(400).json({ error: '׊ם האירו×ĸ הוא שדה חובה' }); + return; + } + if (eventData.votingStands === undefined || eventData.votingStands === null) { + res.status(400).json({ error: 'מספר ×ĸמדו×Ē ×”×Ļב×ĸה הוא שדה חובה' }); + return; + } + return; } + console.log(eventData); + eventData.startDate = new Date(); eventData.endDate = new Date(); console.log(`🔍 Validating Event data: ${JSON.stringify(eventData)}`); - console.log('âŦ Creating Event...'); const eventResult = await db.addElectionEvent(eventData as ElectionEvent); + const eventId = eventResult.insertedId; + if (!eventResult.acknowledged) { console.log('❌ Could not create Event'); res.status(500).json({ ok: false }); @@ -55,7 +68,7 @@ router.post( console.log('✅ Created Event!'); console.log('🔐 Creating division state'); - if (!(await db.addElectionState(getInitialDivisionState())).acknowledged) { + if (!(await db.addElectionState(getInitialDivisionState(eventId))).acknowledged) { throw new Error('Could not create division state!'); } console.log('✅ Created division state'); @@ -68,7 +81,14 @@ router.post( // } console.log('👤 Generating division users'); - const users = CreateVotingStandUsers(eventData.votingStands); + + console.log( + `Creating voting stand users for ${eventData.votingStands} stands with event ID: ${eventId}` + ); + + const users = CreateVotingStandUsers(eventData.votingStands, eventId); + + console.log('users:', users); if (!(await db.addUsers(users)).acknowledged) { res.status(500).json({ error: 'Could not create users!' }); @@ -81,7 +101,7 @@ router.post( ); router.put( - '/', + '/:eventId', asyncHandler(async (req: Request, res: Response) => { const body = req.body; @@ -147,11 +167,11 @@ router.put( ); router.delete( - '/data', + '/:eventId', asyncHandler(async (req: Request, res: Response) => { console.log(`🚮 Deleting data from event`); try { - await cleanDivisionData(); + await cleanDivisionData(new ObjectId(req.params.eventId)); } catch (error) { res.status(500).json(error.message); return; @@ -161,7 +181,8 @@ router.delete( }) ); -router.use('/users', divisionUsersRouter); -router.use('/cities', citiesRouter); +router.use('/:eventId/users', divisionUsersRouter); + +router.use('/:eventId/cities', citiesRouter); export default router; diff --git a/apps/backend/src/routers/api/admin/events/users.ts b/apps/backend/src/routers/api/admin/events/users.ts index 8392b11..695a1ec 100644 --- a/apps/backend/src/routers/api/admin/events/users.ts +++ b/apps/backend/src/routers/api/admin/events/users.ts @@ -7,7 +7,7 @@ import * as db from '@mtes/database'; const router = express.Router({ mergeParams: true }); router.get('/', (req: Request, res: Response) => { - db.getEventUsers().then(users => { + db.getEventUsers(new ObjectId(req.params.eventId)).then(users => { return res.json(users); }); }); @@ -15,7 +15,13 @@ router.get('/', (req: Request, res: Response) => { router.get( '/credentials', asyncHandler(async (req: Request, res: Response) => { - const usersWithAdmin = await db.getEventUsersWithCredentials(); + const eventId = req.params.eventId; + if (!eventId) { + console.log('❌ Event ID is null or undefined'); + res.status(400).json({ ok: false, message: 'Event ID is missing' }); + return; + } + const usersWithAdmin = await db.getEventUsersWithCredentials(new ObjectId(eventId)); const users = usersWithAdmin.filter(user => !user.isAdmin); res.json(users); diff --git a/apps/backend/src/routers/api/admin/password.ts b/apps/backend/src/routers/api/admin/password.ts index acaa7c1..4f90ab3 100644 --- a/apps/backend/src/routers/api/admin/password.ts +++ b/apps/backend/src/routers/api/admin/password.ts @@ -6,90 +6,89 @@ const router = express.Router({ mergeParams: true }); // Update admin password router.put( - '/', - asyncHandler(async (req: Request, res: Response) => { - const { currentPassword, newPassword } = req.body; + '/', + asyncHandler(async (req: Request, res: Response) => { + const { currentPassword, newPassword } = req.body; - if (!currentPassword || !newPassword) { - res.status(400).json({ - error: 'MISSING_FIELDS', - message: 'Current password and new password are required' - }); - return; - } - - if (newPassword.length < 4) { - res.status(400).json({ - error: 'WEAK_PASSWORD', - message: 'New password must be at least 4 characters long' - }); - return; - } + if (!currentPassword || !newPassword) { + res.status(400).json({ + error: 'MISSING_FIELDS', + message: 'Current password and new password are required' + }); + return; + } - try { - // Ensure the user is authenticated and is an admin - if (!req.user || !req.user.isAdmin) { - res.status(403).json({ - error: 'FORBIDDEN', - message: 'Only admin users can change passwords' - }); - return; - } + if (newPassword.length < 4) { + res.status(400).json({ + error: 'WEAK_PASSWORD', + message: 'New password must be at least 4 characters long' + }); + return; + } - // Get the current admin user - const adminUser = await db.getUserWithCredentials({ - _id: req.user?._id, - isAdmin: true - }); + try { + // Ensure the user is authenticated and is an admin + if (!req.user || !req.user.isAdmin) { + res.status(403).json({ + error: 'FORBIDDEN', + message: 'Only admin users can change passwords' + }); + return; + } - if (!adminUser) { - res.status(404).json({ - error: 'USER_NOT_FOUND', - message: 'Admin user not found' - }); - return; - } + // Get the current admin user + const adminUser = await db.getUserWithCredentials({ + _id: req.user?._id, + isAdmin: true + }); - // Verify current password - if (adminUser.password !== currentPassword) { - res.status(401).json({ - error: 'INVALID_CURRENT_PASSWORD', - message: 'Current password is incorrect' - }); - return; - } + if (!adminUser) { + res.status(404).json({ + error: 'USER_NOT_FOUND', + message: 'Admin user not found' + }); + return; + } - // Update password - const updateResult = await db.updateUser( - { _id: adminUser._id }, - { - password: newPassword, - lastPasswordSetDate: new Date() - } - ); + // Verify current password + if (adminUser.password !== currentPassword) { + res.status(401).json({ + error: 'INVALID_CURRENT_PASSWORD', + message: 'Current password is incorrect' + }); + return; + } - if (!updateResult.acknowledged || updateResult.matchedCount === 0) { - res.status(500).json({ - error: 'UPDATE_FAILED', - message: 'Failed to update password' - }); - return; - } + // Update password + const updateResult = await db.updateUser( + { _id: adminUser._id }, + { + password: newPassword, + lastPasswordSetDate: new Date() + } + ); - console.log('✅ Admin password updated successfully'); - res.json({ - ok: true, - message: 'Password updated successfully' - }); + if (!updateResult.acknowledged || updateResult.matchedCount === 0) { + res.status(500).json({ + error: 'UPDATE_FAILED', + message: 'Failed to update password' + }); + return; + } - } catch (error) { - console.error('❌ Error updating admin password:', error); - res.status(500).json({ - error: 'INTERNAL_ERROR', - message: 'Internal server error while updating password' - }); - } - }) + console.log('✅ Admin password updated successfully'); + res.json({ + ok: true, + message: 'Password updated successfully' + }); + } catch (error) { + console.error('❌ Error updating admin password:', error); + res.status(500).json({ + error: 'INTERNAL_ERROR', + message: 'Internal server error while updating password' + }); + } + }) ); export default router; diff --git a/apps/backend/src/routers/api/events/index.ts b/apps/backend/src/routers/api/events/index.ts index 1523bbd..1477b3d 100644 --- a/apps/backend/src/routers/api/events/index.ts +++ b/apps/backend/src/routers/api/events/index.ts @@ -1,16 +1,33 @@ -import express from 'express'; +import express, { Request, Response } from 'express'; import roundsRouter from './rounds'; import membersRouter from './members'; import stateRouter from './state'; import voteRouter from './vote'; import mmMembersRouter from './mm-members'; +import * as db from '@mtes/database'; +import { ObjectId } from 'mongodb'; const router = express.Router({ mergeParams: true }); -router.use('/rounds', roundsRouter); -router.use('/members', membersRouter); -router.use('/mm-members', mmMembersRouter); -router.use('/state', stateRouter); -router.use('/vote', voteRouter); +router.get('/:eventId', async (req: Request, res: Response) => { + const eventId = req.params.eventId; + let event; + try { + event = await db.getElectionEvent(new ObjectId(eventId)); + } catch (err) { + // Invalid ObjectId or db error + return res.status(404).json({ ok: false, message: 'Event not found' }); + } + if (!event) { + return res.status(404).json({ ok: false, message: 'Event not found' }); + } + res.json(event); +}); + +router.use('/:eventId/rounds', roundsRouter); +router.use('/:eventId/members', membersRouter); +router.use('/:eventId/mm-members', mmMembersRouter); +router.use('/:eventId/state', stateRouter); +router.use('/:eventId/vote', voteRouter); export default router; diff --git a/apps/backend/src/routers/api/events/members.ts b/apps/backend/src/routers/api/events/members.ts index e647ed6..02a48e9 100644 --- a/apps/backend/src/routers/api/events/members.ts +++ b/apps/backend/src/routers/api/events/members.ts @@ -6,91 +6,117 @@ import { Member } from '@mtes/types'; const router = express.Router({ mergeParams: true }); router.get('/', async (req: Request, res: Response) => { - console.log('âŦ Getting members...'); - return res.json(await db.getMembers({})); + console.log('âŦ Getting members...'); + return res.json( + await db.getMembers({ + eventId: new ObjectId(req.params.eventId) + }) + ); }); router.put('/', async (req: Request, res: Response) => { - const { members } = req.body as { members: Member[] }; - - if (!members || members.length === 0) { - console.log('❌ Members array is empty'); - res.status(400).json({ ok: false, message: 'No members provided' }); - return; - } - - console.log('âŦ Updating Members...'); - - const deleteRes = await db.deleteMembers({}); - if (!deleteRes.acknowledged) { - console.log('❌ Could not delete members'); - res.status(500).json({ ok: false, message: 'Could not delete members' }); - return; - } - - const addRes = await db.addMembers(members.map(member => ({ ...member, _id: undefined }))); - if (!addRes.acknowledged) { - console.log('❌ Could not add members'); - res.status(500).json({ ok: false, message: 'Could not add members' }); - return; - } - - console.log('âŦ Members updated!'); - res.json({ ok: true }); + const eventId = req.params.eventId; + if (!eventId) { + console.log('❌ Event ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'Event ID is missing' }); + } + + const { members } = req.body as { members: Member[] }; + if (!members || members.length === 0) { + console.log('❌ Members array is empty'); + res.status(400).json({ ok: false, message: 'No members provided' }); + return; + } + + console.log('âŦ Updating Members...'); + + const deleteRes = await db.deleteMembers({ + eventId: new ObjectId(eventId) + }); + if (!deleteRes.acknowledged) { + console.log('❌ Could not delete members'); + res.status(500).json({ ok: false, message: 'Could not delete members' }); + return; + } + + const addRes = await db.addMembers( + members.map(member => ({ ...member, eventId: new ObjectId(eventId) })) + ); + if (!addRes.acknowledged) { + console.log('❌ Could not add members'); + res.status(500).json({ ok: false, message: 'Could not add members' }); + return; + } + + console.log('âŦ Members updated!'); + res.json({ ok: true }); }); router.put('/:memberId/presence', async (req: Request, res: Response) => { - const { memberId } = req.params; - const { isPresent, replacedBy } = req.body as { isPresent: boolean; replacedBy?: WithId }; - - if (!memberId) { - console.log('❌ Member ID is null or undefined'); - return res.status(400).json({ ok: false, message: 'Member ID is missing' }); + const { memberId } = req.params; + const { isPresent, replacedBy } = req.body as { isPresent: boolean; replacedBy?: WithId }; + + if (!memberId) { + console.log('❌ Member ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'Member ID is missing' }); + } + + if (typeof isPresent !== 'boolean' && replacedBy === undefined) { + console.log('❌ isPresent is missing or not a boolean, and replacedBy is not provided'); + return res + .status(400) + .json({ + ok: false, + message: 'isPresent field (boolean) or replacedBy field (string) is required' + }); + } + + if (replacedBy && typeof replacedBy !== 'object') { + console.log('❌ replacedBy is not a WithId'); + return res + .status(400) + .json({ ok: false, message: 'replacedBy field must be a WithId' }); + } + + let updatePayload: { isPresent: boolean; replacedBy: WithId | null } = { + isPresent, + replacedBy: null + }; + + if (replacedBy) { + console.log( + `âŦ Member ${memberId} is being replaced by ${replacedBy._id}. Setting isPresent to true.` + ); + updatePayload = { isPresent: true, replacedBy: replacedBy as WithId }; + } else { + console.log(`âŦ Updating presence for member ${memberId} to ${isPresent}`); + updatePayload = { isPresent, replacedBy: null }; + } + + try { + const memberResult = await db.updateMember( + { _id: new ObjectId(memberId) }, + updatePayload as unknown as Partial + ); + + if (!memberResult.acknowledged || memberResult.matchedCount === 0) { + console.log( + `❌ Could not update presence for member ${memberId}. Member not found or update failed.` + ); + return res.status(404).json({ + ok: false, + message: 'Could not update member presence. Member not found or update failed.' + }); } - if (typeof isPresent !== 'boolean' && replacedBy === undefined) { - console.log('❌ isPresent is missing or not a boolean, and replacedBy is not provided'); - return res.status(400).json({ ok: false, message: 'isPresent field (boolean) or replacedBy field (string) is required' }); - } - - if (replacedBy && (typeof replacedBy !== 'object')) { - console.log('❌ replacedBy is not a WithId'); - return res.status(400).json({ ok: false, message: 'replacedBy field must be a WithId' }); - } - - let updatePayload: { isPresent: boolean; replacedBy: WithId | null } = { isPresent, replacedBy: null }; - - - if (replacedBy) { - console.log(`âŦ Member ${memberId} is being replaced by ${replacedBy._id}. Setting isPresent to true.`); - updatePayload = { isPresent: true, replacedBy: replacedBy as WithId }; - } else { - console.log(`âŦ Updating presence for member ${memberId} to ${isPresent}`); - updatePayload = { isPresent, replacedBy: null }; - } - - - try { - const memberResult = await db.updateMember({ _id: new ObjectId(memberId) }, updatePayload as unknown as Partial); - - if (!memberResult.acknowledged || memberResult.matchedCount === 0) { - console.log( - `❌ Could not update presence for member ${memberId}. Member not found or update failed.` - ); - return res.status(404).json({ - ok: false, - message: 'Could not update member presence. Member not found or update failed.' - }); - } - - console.log(`✅ Presence updated for member ${memberId}`); - res.json({ ok: true }); - } catch (error) { - console.error('❌ Error updating member presence:', error); - return res - .status(500) - .json({ ok: false, message: 'Internal server error while updating member presence' }); - } + console.log(`✅ Presence updated for member ${memberId}`); + res.json({ ok: true }); + } catch (error) { + console.error('❌ Error updating member presence:', error); + return res + .status(500) + .json({ ok: false, message: 'Internal server error while updating member presence' }); + } }); export default router; diff --git a/apps/backend/src/routers/api/events/mm-members.ts b/apps/backend/src/routers/api/events/mm-members.ts index d365eb2..30c4ba6 100644 --- a/apps/backend/src/routers/api/events/mm-members.ts +++ b/apps/backend/src/routers/api/events/mm-members.ts @@ -6,80 +6,102 @@ import { Member } from '@mtes/types'; const router = express.Router({ mergeParams: true }); router.get('/', async (req: Request, res: Response) => { - console.log('âŦ Getting mm-members...'); - return res.json(await db.getMmMembers({})); + const eventId = req.params.eventId; + if (!eventId) { + console.log('❌ Event ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'Event ID is missing' }); + } + + console.log('âŦ Getting mm-members...'); + return res.json( + await db.getMmMembers({ + eventId: new ObjectId(eventId) + }) + ); }); router.put('/', async (req: Request, res: Response) => { - const { mmMembers } = req.body as { mmMembers: Member[] } || { mmMembers: [] }; - - console.log('âŦ Updating mm-members...'); - - if (!Array.isArray(mmMembers)) { - console.log('❌ Invalid mmMembers format: not an array'); - return res.status(400).json({ ok: false, message: 'mmMembers must be an array' }); + const eventId = req.params.eventId; + if (!eventId) { + console.log('❌ Event ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'Event ID is missing' }); + } + + const { mmMembers } = (req.body as { mmMembers: Member[] }) || { mmMembers: [] }; + + console.log('âŦ Updating mm-members...'); + + if (!Array.isArray(mmMembers)) { + console.log('❌ Invalid mmMembers format: not an array'); + return res.status(400).json({ ok: false, message: 'mmMembers must be an array' }); + } + + try { + const deleteRes = await db.deleteMmMembers({ + eventId: new ObjectId(eventId) + }); + if (!deleteRes.acknowledged) { + console.log('❌ Could not delete mm-members'); + return res.status(500).json({ ok: false, message: 'Could not delete mm-members' }); } - try { - const deleteRes = await db.deleteMmMembers({}); - if (!deleteRes.acknowledged) { - console.log('❌ Could not delete mm-members'); - return res.status(500).json({ ok: false, message: 'Could not delete mm-members' }); - } - - if (mmMembers.length > 0) { - const addRes = await db.addMmMembers(mmMembers.map(member => ({ ...member, _id: undefined }))); - if (!addRes.acknowledged) { - console.log('❌ Could not add mm-members'); - return res.status(500).json({ ok: false, message: 'Could not add mm-members' }); - } - } - - console.log(`✅ Successfully updated mm-members (${mmMembers.length} members)`); - return res.json({ ok: true }); - } catch (error) { - console.error('❌ Error updating mm-members:', error); - return res.status(500).json({ ok: false, message: 'Internal server error while updating mm-members' }); + if (mmMembers.length > 0) { + const addRes = await db.addMmMembers( + mmMembers.map(member => ({ ...member, eventId: new ObjectId(eventId) })) + ); + if (!addRes.acknowledged) { + console.log('❌ Could not add mm-members'); + return res.status(500).json({ ok: false, message: 'Could not add mm-members' }); + } } + + console.log(`✅ Successfully updated mm-members (${mmMembers.length} members)`); + return res.json({ ok: true }); + } catch (error) { + console.error('❌ Error updating mm-members:', error); + return res + .status(500) + .json({ ok: false, message: 'Internal server error while updating mm-members' }); + } }); router.put('/:mmMemberId/presence', async (req: Request, res: Response) => { - const { mmMemberId } = req.params; - const { isPresent } = req.body as { isPresent: boolean }; - - if (!mmMemberId) { - console.log('❌ mmMember ID is null or undefined'); - return res.status(400).json({ ok: false, message: 'mmMember ID is missing' }); + const { mmMemberId } = req.params; + const { isPresent } = req.body as { isPresent: boolean }; + + if (!mmMemberId) { + console.log('❌ mmMember ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'mmMember ID is missing' }); + } + + if (typeof isPresent !== 'boolean') { + console.log('❌ isPresent is missing or not a boolean'); + return res.status(400).json({ ok: false, message: 'isPresent field (boolean) is required' }); + } + + console.log(`âŦ Updating presence for mm-member ${mmMemberId} to ${isPresent}`); + + try { + const memberResult = await db.updateMmMember({ _id: new ObjectId(mmMemberId) }, { isPresent }); + + if (!memberResult.acknowledged || memberResult.matchedCount === 0) { + console.log( + `❌ Could not update presence for mm-member ${mmMemberId}. mm-member not found or update failed.` + ); + return res.status(404).json({ + ok: false, + message: 'Could not update mm-member presence. mm-member not found or update failed.' + }); } - if (typeof isPresent !== 'boolean') { - console.log('❌ isPresent is missing or not a boolean'); - return res.status(400).json({ ok: false, message: 'isPresent field (boolean) is required' }); - } - - console.log(`âŦ Updating presence for mm-member ${mmMemberId} to ${isPresent}`); - - try { - const memberResult = await db.updateMmMember({ _id: new ObjectId(mmMemberId) }, { isPresent }); - - if (!memberResult.acknowledged || memberResult.matchedCount === 0) { - console.log( - `❌ Could not update presence for mm-member ${mmMemberId}. mm-member not found or update failed.` - ); - return res.status(404).json({ - ok: false, - message: 'Could not update mm-member presence. mm-member not found or update failed.' - }); - } - - console.log(`✅ Presence updated for mm-member ${mmMemberId}`); - res.json({ ok: true }); - } catch (error) { - console.error('❌ Error updating mm-member presence:', error); - return res - .status(500) - .json({ ok: false, message: 'Internal server error while updating mm-member presence' }); - } + console.log(`✅ Presence updated for mm-member ${mmMemberId}`); + res.json({ ok: true }); + } catch (error) { + console.error('❌ Error updating mm-member presence:', error); + return res + .status(500) + .json({ ok: false, message: 'Internal server error while updating mm-member presence' }); + } }); export default router; diff --git a/apps/backend/src/routers/api/events/rounds.ts b/apps/backend/src/routers/api/events/rounds.ts index ad6317b..7a41fb2 100644 --- a/apps/backend/src/routers/api/events/rounds.ts +++ b/apps/backend/src/routers/api/events/rounds.ts @@ -5,354 +5,378 @@ import { Member, Round } from '@mtes/types'; const router = express.Router({ mergeParams: true }); -const genrateWhireVoteMembers = (numWhiteVotes: number): WithId[] => { - const whiteVotes: WithId[] = []; - for (let i = 0; i < numWhiteVotes; i++) { - whiteVotes.push( - { - _id: new ObjectId(`00000000000000000000000${i + 1}`), - name: `פ×Ē×§ לבן ${i + 1}`, - city: 'אין אמון באת אחד', - isPresent: true, - isMM: false, - } - ); - } - return whiteVotes; -} +const genrateWhireVoteMembers = (numWhiteVotes: number, eventId: ObjectId): WithId[] => { + const whiteVotes: WithId[] = []; + for (let i = 0; i < numWhiteVotes; i++) { + whiteVotes.push({ + _id: new ObjectId(`00000000000000000000000${i + 1}`), + eventId: eventId, + name: `פ×Ē×§ לבן ${i + 1}`, + city: 'אין אמון באת אחד', + isPresent: true, + isMM: false + }); + } + return whiteVotes; +}; router.get('/', async (req: Request, res: Response) => { - console.log('âŦ Getting rounds...'); - return res.json(await db.getRounds({})); + const eventId = req.params.eventId; + if (!eventId) { + console.log('❌ Event ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'Event ID is missing' }); + } + + console.log('âŦ Getting rounds...'); + return res.json( + await db.getRounds({ + eventId: new ObjectId(eventId) + }) + ); }); router.post('/add', async (req: Request, res: Response) => { - const { round } = req.body; - - console.log('âŦ Adding Round...', JSON.stringify(round, null, 2)); - - if (!round) { - console.log('❌ Round object is null or undefined'); - return res.status(400).json({ ok: false, message: 'Round object is missing' }); - } - if (!round.name) { - console.log('❌ Round name is missing or empty'); - return res.status(400).json({ ok: false, message: 'Round name is required' }); - } - if (!round.roles) { - console.log('❌ Round roles are missing'); - return res.status(400).json({ ok: false, message: 'Round roles must be specified' }); - } - if (!round.allowedMembers) { - console.log('❌ Round allowedMembers is missing'); - return res.status(400).json({ ok: false, message: 'Round allowed members must be specified' }); - } - - try { - round.allowedMembers = await Promise.all( - round.allowedMembers.map(async (member: string | { _id: string }) => { - const memberId = typeof member === 'string' ? member : member._id; - const dbMember = await db.getMember({ _id: new ObjectId(memberId) }); - if (!dbMember) { - console.log(`❌ Member with ID ${memberId} not found`); - // Decide how to handle this: throw error, return null and filter later, or return error response - throw new Error(`Member with ID ${memberId} not found`); - } - return dbMember; - }) - ); - - round.roles = await Promise.all( - round.roles.map(async (role: any) => { - const contestants = await Promise.all( - role.contestants.map(async (contestant: string | { _id: string }) => { - const contestantId = typeof contestant === 'string' ? contestant : contestant._id; - const dbContestant = await db.getMember({ _id: new ObjectId(contestantId) }); - if (!dbContestant) { - console.log(`❌ Contestant with ID ${contestantId} in role ${role.role} not found`); - throw new Error(`Contestant with ID ${contestantId} in role ${role.role} not found`); - } - return dbContestant; - }) - ); - if (role.numWhiteVotes > 0) { - contestants.push(...genrateWhireVoteMembers(role.numWhiteVotes)); - } - return { ...role, contestants }; - }) + const eventId = req.params.eventId; + if (!eventId) { + console.log('❌ Event ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'Event ID is missing' }); + } + + const { round } = req.body; + round.eventId = new ObjectId(eventId); + + console.log('âŦ Adding Round...', JSON.stringify(round, null, 2)); + + if (!round) { + console.log('❌ Round object is null or undefined'); + return res.status(400).json({ ok: false, message: 'Round object is missing' }); + } + if (!round.name) { + console.log('❌ Round name is missing or empty'); + return res.status(400).json({ ok: false, message: 'Round name is required' }); + } + if (!round.roles) { + console.log('❌ Round roles are missing'); + return res.status(400).json({ ok: false, message: 'Round roles must be specified' }); + } + if (!round.allowedMembers) { + console.log('❌ Round allowedMembers is missing'); + return res.status(400).json({ ok: false, message: 'Round allowed members must be specified' }); + } + + try { + round.allowedMembers = await Promise.all( + round.allowedMembers.map(async (member: string | { _id: string }) => { + const memberId = typeof member === 'string' ? member : member._id; + const dbMember = await db.getMember({ _id: new ObjectId(memberId) }); + if (!dbMember) { + console.log(`❌ Member with ID ${memberId} not found`); + // Decide how to handle this: throw error, return null and filter later, or return error response + throw new Error(`Member with ID ${memberId} not found`); + } + return dbMember; + }) + ); + + round.roles = await Promise.all( + round.roles.map(async (role: any) => { + const contestants = await Promise.all( + role.contestants.map(async (contestant: string | { _id: string }) => { + const contestantId = typeof contestant === 'string' ? contestant : contestant._id; + const dbContestant = await db.getMember({ _id: new ObjectId(contestantId) }); + if (!dbContestant) { + console.log(`❌ Contestant with ID ${contestantId} in role ${role.role} not found`); + throw new Error(`Contestant with ID ${contestantId} in role ${role.role} not found`); + } + return dbContestant; + }) ); - - console.log('âŦ Adding Round to db...', JSON.stringify(round, null, 2)); - const roundResult = await db.addRound(round); - - res.json({ ok: true, id: roundResult.insertedId }); - } catch (error: any) { - console.error('❌ Error adding round:', error); - return res.status(error.message.includes('not found') ? 400 : 500).json({ ok: false, message: error.message || 'Internal server error' }); - } + if (role.numWhiteVotes > 0) { + contestants.push(...genrateWhireVoteMembers(role.numWhiteVotes, round.eventId)); + } + return { ...role, contestants }; + }) + ); + + console.log('âŦ Adding Round to db...', JSON.stringify(round, null, 2)); + const roundResult = await db.addRound(round); + + res.json({ ok: true, id: roundResult.insertedId }); + } catch (error: any) { + console.error('❌ Error adding round:', error); + return res + .status(error.message.includes('not found') ? 400 : 500) + .json({ ok: false, message: error.message || 'Internal server error' }); + } }); // Type for the update request body - uses string IDs instead of full objects interface UpdateRoundRequest { - name?: string; - allowedMembers?: string[]; - roles?: { - role: string; - contestants: string[]; - maxVotes: number; - numWhiteVotes: number; - numWinners: number; - }[]; - startTime?: Date | null; - endTime?: Date | null; - isLocked?: boolean; + name?: string; + allowedMembers?: string[]; + roles?: { + role: string; + contestants: string[]; + maxVotes: number; + numWhiteVotes: number; + numWinners: number; + }[]; + startTime?: Date | null; + endTime?: Date | null; + isLocked?: boolean; } router.put('/update', async (req: Request, res: Response) => { - const { roundId, round } = req.body as { roundId: string; round: UpdateRoundRequest }; - - if (!roundId) { - console.log('❌ Round ID is null or undefined'); - res.status(400).json({ ok: false, message: 'Round ID is missing' }); - return; + const { roundId, round } = req.body as { roundId: string; round: UpdateRoundRequest }; + + if (!roundId) { + console.log('❌ Round ID is null or undefined'); + res.status(400).json({ ok: false, message: 'Round ID is missing' }); + return; + } + + if (!round || Object.keys(round).length === 0) { + console.log('❌ Round update object is empty'); + res.status(400).json({ ok: false, message: 'No changes provided' }); + return; + } + + try { + // Check if the round exists and hasn't started yet + const existingRound = await db.getRound({ _id: new ObjectId(roundId) }); + if (!existingRound) { + console.log(`❌ Round with ID ${roundId} not found`); + return res.status(404).json({ ok: false, message: 'Round not found' }); } - if (!round || Object.keys(round).length === 0) { - console.log('❌ Round update object is empty'); - res.status(400).json({ ok: false, message: 'No changes provided' }); - return; + // Prevent editing rounds that have already started + if (existingRound.startTime) { + console.log(`❌ Cannot edit round ${roundId} - round has already started`); + return res + .status(400) + .json({ ok: false, message: 'Cannot edit round that has already started' }); } - try { - // Check if the round exists and hasn't started yet - const existingRound = await db.getRound({ _id: new ObjectId(roundId) }); - if (!existingRound) { - console.log(`❌ Round with ID ${roundId} not found`); - return res.status(404).json({ ok: false, message: 'Round not found' }); - } - - // Prevent editing rounds that have already started - if (existingRound.startTime) { - console.log(`❌ Cannot edit round ${roundId} - round has already started`); - return res.status(400).json({ ok: false, message: 'Cannot edit round that has already started' }); - } - - // Prevent editing locked rounds - if (existingRound.isLocked) { - console.log(`❌ Cannot edit round ${roundId} - round is locked`); - return res.status(400).json({ ok: false, message: 'Cannot edit locked round' }); - } - - // Check if round has any votes (active voting has occurred) - const votedMembers = await db.getVotedMembers(roundId); - if (votedMembers && votedMembers.length > 0) { - console.log(`❌ Cannot edit round ${roundId} - round has active votes`); - return res.status(400).json({ ok: false, message: 'Cannot edit round that has active votes' }); - } - - console.log('âŦ Updating Round...'); - console.log('Changes:', round); - - // Create the processed round object for database update - const processedRound: Partial = {}; - - // Copy simple fields - if (round.name !== undefined) processedRound.name = round.name; - if (round.startTime !== undefined) processedRound.startTime = round.startTime; - if (round.endTime !== undefined) processedRound.endTime = round.endTime; - if (round.isLocked !== undefined) processedRound.isLocked = round.isLocked; - - // Process allowedMembers if they are being updated - if (round.allowedMembers) { - processedRound.allowedMembers = await Promise.all( - round.allowedMembers.map(async (memberId: string) => { - const dbMember = await db.getMember({ _id: new ObjectId(memberId) }); - if (!dbMember) { - console.log(`❌ Member with ID ${memberId} not found`); - throw new Error(`Member with ID ${memberId} not found`); - } - return dbMember; - }) - ); - } - - // Process roles if they are being updated - if (round.roles) { - processedRound.roles = await Promise.all( - round.roles.map(async (role: any) => { - const contestants = await Promise.all( - role.contestants.map(async (contestantId: string) => { - const dbContestant = await db.getMember({ _id: new ObjectId(contestantId) }); - if (!dbContestant) { - console.log(`❌ Contestant with ID ${contestantId} in role ${role.role} not found`); - throw new Error(`Contestant with ID ${contestantId} in role ${role.role} not found`); - } - return dbContestant; - }) - ); - if (role.numWhiteVotes > 0) { - contestants.push(...genrateWhireVoteMembers(role.numWhiteVotes)); - } - return { ...role, contestants }; - }) - ); - } - - const roundResult = await db.updateRound({ _id: new ObjectId(roundId) }, processedRound); + // Prevent editing locked rounds + if (existingRound.isLocked) { + console.log(`❌ Cannot edit round ${roundId} - round is locked`); + return res.status(400).json({ ok: false, message: 'Cannot edit locked round' }); + } - if (!roundResult.acknowledged) { - console.log(`❌ Could not update Round`); - res.status(500).json({ ok: false, message: 'Could not update round' }); - return; - } + // Check if round has any votes (active voting has occurred) + const votedMembers = await db.getVotedMembers(roundId); + if (votedMembers && votedMembers.length > 0) { + console.log(`❌ Cannot edit round ${roundId} - round has active votes`); + return res + .status(400) + .json({ ok: false, message: 'Cannot edit round that has active votes' }); + } - res.json({ ok: true }); - } catch (error: any) { - console.error('❌ Error updating round:', error); - return res.status(500).json({ ok: false, message: error.message || 'Internal server error' }); + console.log('âŦ Updating Round...'); + console.log('Changes:', round); + + // Create the processed round object for database update + const processedRound: Partial = {}; + + // Copy simple fields + if (round.name !== undefined) processedRound.name = round.name; + if (round.startTime !== undefined) processedRound.startTime = round.startTime; + if (round.endTime !== undefined) processedRound.endTime = round.endTime; + if (round.isLocked !== undefined) processedRound.isLocked = round.isLocked; + + // Process allowedMembers if they are being updated + if (round.allowedMembers) { + processedRound.allowedMembers = await Promise.all( + round.allowedMembers.map(async (memberId: string) => { + const dbMember = await db.getMember({ _id: new ObjectId(memberId) }); + if (!dbMember) { + console.log(`❌ Member with ID ${memberId} not found`); + throw new Error(`Member with ID ${memberId} not found`); + } + return dbMember; + }) + ); } -}); -router.delete('/delete', async (req: Request, res: Response) => { - const { roundId } = req.body as { roundId: string }; - if (!roundId) { - console.log('❌ Round ID is null or undefined'); - res.status(400).json({ ok: false, message: 'Round ID is missing' }); - return; + // Process roles if they are being updated + if (round.roles) { + processedRound.roles = await Promise.all( + round.roles.map(async (role: any) => { + const contestants = await Promise.all( + role.contestants.map(async (contestantId: string) => { + const dbContestant = await db.getMember({ _id: new ObjectId(contestantId) }); + if (!dbContestant) { + console.log(`❌ Contestant with ID ${contestantId} in role ${role.role} not found`); + throw new Error( + `Contestant with ID ${contestantId} in role ${role.role} not found` + ); + } + return dbContestant; + }) + ); + if (role.numWhiteVotes > 0) { + contestants.push(...genrateWhireVoteMembers(role.numWhiteVotes, existingRound.eventId)); + } + return { ...role, contestants }; + }) + ); } - console.log('âŦ Deleting Round...'); - const roundResult = await db.deleteRound({ _id: new ObjectId(roundId) }); + const roundResult = await db.updateRound({ _id: new ObjectId(roundId) }, processedRound); + if (!roundResult.acknowledged) { - console.log(`❌ Could not delete Round`); - res.status(500).json({ ok: false, message: 'Could not delete round' }); - return; + console.log(`❌ Could not update Round`); + res.status(500).json({ ok: false, message: 'Could not update round' }); + return; } res.json({ ok: true }); + } catch (error: any) { + console.error('❌ Error updating round:', error); + return res.status(500).json({ ok: false, message: error.message || 'Internal server error' }); + } }); -router.post('/lock/:roundId', async (req: Request, res: Response) => { - const { roundId } = req.params; +router.delete('/delete', async (req: Request, res: Response) => { + const { roundId } = req.body as { roundId: string }; + if (!roundId) { + console.log('❌ Round ID is null or undefined'); + res.status(400).json({ ok: false, message: 'Round ID is missing' }); + return; + } + console.log('âŦ Deleting Round...'); + + const roundResult = await db.deleteRound({ _id: new ObjectId(roundId) }); + if (!roundResult.acknowledged) { + console.log(`❌ Could not delete Round`); + res.status(500).json({ ok: false, message: 'Could not delete round' }); + return; + } + + res.json({ ok: true }); +}); - if (!roundId) { - console.log('❌ Round ID is null or undefined'); - res.status(400).json({ ok: false, message: 'Round ID is missing' }); - return; +router.post('/lock/:roundId', async (req: Request, res: Response) => { + const { roundId } = req.params; + + if (!roundId) { + console.log('❌ Round ID is null or undefined'); + res.status(400).json({ ok: false, message: 'Round ID is missing' }); + return; + } + + try { + const isLocked = await db.isRoundLocked(roundId); + if (isLocked) { + console.log(`❌ Round ${roundId} is already locked`); + return res.status(400).json({ ok: false, message: 'Round is already locked' }); } - try { - const isLocked = await db.isRoundLocked(roundId); - if (isLocked) { - console.log(`❌ Round ${roundId} is already locked`); - return res.status(400).json({ ok: false, message: 'Round is already locked' }); - } - - // Set both isLocked and endTime when locking a round - await db.lockRound(roundId); - await db.updateRound({ _id: new ObjectId(roundId) }, { endTime: new Date() }); - console.log(`✅ Round ${roundId} locked successfully`); - return res.json({ ok: true }); - } catch (error) { - console.error('❌ Error locking round:', error); - return res.status(500).json({ ok: false, message: 'Internal server error' }); - } + // Set both isLocked and endTime when locking a round + await db.lockRound(roundId); + await db.updateRound({ _id: new ObjectId(roundId) }, { endTime: new Date() }); + console.log(`✅ Round ${roundId} locked successfully`); + return res.json({ ok: true }); + } catch (error) { + console.error('❌ Error locking round:', error); + return res.status(500).json({ ok: false, message: 'Internal server error' }); + } }); router.post('/unlock/:roundId', async (req: Request, res: Response) => { - const { roundId } = req.params; - - if (!roundId) { - console.log('❌ Round ID is null or undefined'); - res.status(400).json({ ok: false, message: 'Round ID is missing' }); - return; + const { roundId } = req.params; + + if (!roundId) { + console.log('❌ Round ID is null or undefined'); + res.status(400).json({ ok: false, message: 'Round ID is missing' }); + return; + } + + try { + const isLocked = await db.isRoundLocked(roundId); + if (!isLocked) { + console.log(`❌ Round ${roundId} is not locked`); + return res.status(400).json({ ok: false, message: 'Round is not locked' }); } - try { - const isLocked = await db.isRoundLocked(roundId); - if (!isLocked) { - console.log(`❌ Round ${roundId} is not locked`); - return res.status(400).json({ ok: false, message: 'Round is not locked' }); - } - - // Unlock the round and clear endTime, but keep startTime - await db.unlockRound(roundId); - await db.updateRound({ _id: new ObjectId(roundId) }, { endTime: null }); + // Unlock the round and clear endTime, but keep startTime + await db.unlockRound(roundId); + await db.updateRound({ _id: new ObjectId(roundId) }, { endTime: null }); - // Delete all votes for this round when unlocking - await db.deleteRoundVotes(roundId); + // Delete all votes for this round when unlocking + await db.deleteRoundVotes(roundId); - console.log(`✅ Round ${roundId} unlocked successfully`); - return res.json({ ok: true }); - } catch (error) { - console.error('❌ Error unlocking round:', error); - return res.status(500).json({ ok: false, message: 'Internal server error' }); - } + console.log(`✅ Round ${roundId} unlocked successfully`); + return res.json({ ok: true }); + } catch (error) { + console.error('❌ Error unlocking round:', error); + return res.status(500).json({ ok: false, message: 'Internal server error' }); + } }); router.get('/status/:roundId', async (req: Request, res: Response) => { - const { roundId } = req.params; - - if (!roundId) { - console.log('❌ Round ID is null or undefined'); - res.status(400).json({ ok: false, message: 'Round ID is missing' }); - return; - } - - try { - const isLocked = await db.isRoundLocked(roundId); - console.log(`✅ Round ${roundId} lock status retrieved`); - return res.json({ ok: true, isLocked }); - } catch (error) { - console.error('❌ Error getting round status:', error); - return res.status(500).json({ ok: false, message: 'Internal server error' }); - } + const { roundId } = req.params; + + if (!roundId) { + console.log('❌ Round ID is null or undefined'); + res.status(400).json({ ok: false, message: 'Round ID is missing' }); + return; + } + + try { + const isLocked = await db.isRoundLocked(roundId); + console.log(`✅ Round ${roundId} lock status retrieved`); + return res.json({ ok: true, isLocked }); + } catch (error) { + console.error('❌ Error getting round status:', error); + return res.status(500).json({ ok: false, message: 'Internal server error' }); + } }); router.get('/results/:roundId', async (req: Request, res: Response) => { - const { roundId } = req.params; - - if (!roundId) { - console.log('❌ Round ID is null or undefined'); - res.status(400).json({ ok: false, message: 'Round ID is missing' }); - return; + const { roundId } = req.params; + + if (!roundId) { + console.log('❌ Round ID is null or undefined'); + res.status(400).json({ ok: false, message: 'Round ID is missing' }); + return; + } + + try { + const isLocked = await db.isRoundLocked(roundId); + if (!isLocked) { + console.log(`❌ Cannot view results for unlocked round ${roundId}`); + return res.status(400).json({ ok: false, message: 'Round must be locked to view results' }); } - try { - const isLocked = await db.isRoundLocked(roundId); - if (!isLocked) { - console.log(`❌ Cannot view results for unlocked round ${roundId}`); - return res.status(400).json({ ok: false, message: 'Round must be locked to view results' }); - } - - const results = await db.getRoundResults(roundId); - if (!results) { - console.log(`❌ Round ${roundId} not found`); - return res.status(404).json({ ok: false, message: 'Round not found' }); - } - - console.log(`✅ Round ${roundId} results retrieved`); - return res.json({ ok: true, results }); - } catch (error) { - console.error('❌ Error getting round results:', error); - return res.status(500).json({ ok: false, message: 'Internal server error' }); + const results = await db.getRoundResults(roundId); + if (!results) { + console.log(`❌ Round ${roundId} not found`); + return res.status(404).json({ ok: false, message: 'Round not found' }); } + + console.log(`✅ Round ${roundId} results retrieved`); + return res.json({ ok: true, results }); + } catch (error) { + console.error('❌ Error getting round results:', error); + return res.status(500).json({ ok: false, message: 'Internal server error' }); + } }); router.get('/votedMembers/:roundId', async (req: Request, res: Response) => { - const { roundId } = req.params; - if (!roundId) { - console.log('❌ Round ID is null or undefined'); - res.status(400).json({ ok: false, message: 'Round ID is missing' }); - return; - } - console.log(`âŦ Getting voted members for round ${roundId}`); - const votedMembers = await db.getVotedMembers(roundId); - if (!votedMembers) { - console.log(`❌ Could not get voted members`); - res.status(500).json({ ok: false, message: 'Could not get voted members' }); - return; - } - res.json({ ok: true, votedMembers }); + const { roundId } = req.params; + if (!roundId) { + console.log('❌ Round ID is null or undefined'); + res.status(400).json({ ok: false, message: 'Round ID is missing' }); + return; + } + console.log(`âŦ Getting voted members for round ${roundId}`); + const votedMembers = await db.getVotedMembers(roundId); + if (!votedMembers) { + console.log(`❌ Could not get voted members`); + res.status(500).json({ ok: false, message: 'Could not get voted members' }); + return; + } + res.json({ ok: true, votedMembers }); }); export default router; diff --git a/apps/backend/src/routers/api/events/state.ts b/apps/backend/src/routers/api/events/state.ts index 6b76a71..fbedbc8 100644 --- a/apps/backend/src/routers/api/events/state.ts +++ b/apps/backend/src/routers/api/events/state.ts @@ -1,28 +1,37 @@ import express, { Request, Response } from 'express'; import * as db from '@mtes/database'; import { ElectionState } from '@mtes/types'; +import { ObjectId } from 'mongodb'; const router = express.Router({ mergeParams: true }); router.get('/', (req: Request, res: Response) => { - console.log(`âŦ Getting Election state`); - db.getElectionState().then(divisionState => res.json(divisionState)); + const eventId = req.params.eventId; + if (!eventId) { + console.log('❌ Event ID is null or undefined'); + return res.status(400).json({ ok: false, message: 'Event ID is missing' }); + } + + console.log(`âŦ Getting Election state`); + db.getElectionState({ + eventId: new ObjectId(eventId) + }).then(divisionState => res.json(divisionState)); }); router.put('/', (req: Request, res: Response) => { - const body: Partial = { ...req.body }; - if (!body) return res.status(400).json({ ok: false }); + const body: Partial = { ...req.body }; + if (!body) return res.status(400).json({ ok: false }); - console.log(`âŦ Updating Election state`); - db.updateElectionState(body).then(task => { - if (task.acknowledged) { - console.log('✅ Election state updated!'); - return res.json({ ok: true, id: task.upsertedId }); - } else { - console.log('❌ Could not update Election state'); - return res.status(500).json({ ok: false }); - } - }); + console.log(`âŦ Updating Election state`); + db.updateElectionState(body).then(task => { + if (task.acknowledged) { + console.log('✅ Election state updated!'); + return res.json({ ok: true, id: task.upsertedId }); + } else { + console.log('❌ Could not update Election state'); + return res.status(500).json({ ok: false }); + } + }); }); export default router; diff --git a/apps/backend/src/routers/api/events/vote.ts b/apps/backend/src/routers/api/events/vote.ts index 6fe5d5a..4294b80 100644 --- a/apps/backend/src/routers/api/events/vote.ts +++ b/apps/backend/src/routers/api/events/vote.ts @@ -5,91 +5,90 @@ import { Member, Positions } from '@mtes/types'; const router = express.Router({ mergeParams: true }); -const getWhiteVoteMember = (id: string): WithId => { - return { - _id: new ObjectId(id), - name: `פ×Ē×§ לבן ${Number(id)}`, - city: 'אין אמון באת אחד', - isPresent: true, - isMM: false, - }; -} +const getWhiteVoteMember = (id: string, eventId?: string): WithId => { + return { + _id: new ObjectId(id), + eventId: eventId ? new ObjectId(eventId) : new ObjectId(), // Provide eventId or fallback + name: `פ×Ē×§ לבן ${Number(id)}`, + city: 'אין אמון באת אחד', + isPresent: true, + isMM: false + }; +}; router.post('/', async (req: Request, res: Response) => { - const { roundId, memberId, votes, votingStandId, signature } = req.body; + const { roundId, memberId, votes, votingStandId, signature } = req.body; - if (!roundId || !memberId || !votes || votingStandId === undefined || signature === undefined) { - console.log('❌ Missing required vote data'); - return res.status(400).json({ ok: false, message: 'Missing required vote data' }); + if (!roundId || !memberId || !votes || votingStandId === undefined || signature === undefined) { + console.log('❌ Missing required vote data'); + return res.status(400).json({ ok: false, message: 'Missing required vote data' }); + } + + try { + const round = await db.getRound({ _id: new ObjectId(roundId) }); + const member = await db.getMember({ _id: new ObjectId(memberId) }); + + if (!round) { + console.log(`❌ Round with ID ${roundId} not found`); + return res.status(404).json({ ok: false, message: 'Round not found' }); } - try { - const round = await db.getRound({ _id: new ObjectId(roundId) }); - const member = await db.getMember({ _id: new ObjectId(memberId) }); - - if (!round) { - console.log(`❌ Round with ID ${roundId} not found`); - return res.status(404).json({ ok: false, message: 'Round not found' }); - } - - if (!member) { - console.log(`❌ Member with ID ${memberId} not found`); - return res.status(404).json({ ok: false, message: 'Member not found' }); - } - - const isLocked = await db.isRoundLocked(roundId); - if (isLocked) { - console.log(`❌ Round ${roundId} is locked`); - return res.status(400).json({ ok: false, message: 'Round is locked' }); - } - - const hasMemberVoted = await db.hasMemberVoted(round._id.toString(), member._id.toString()); - if (hasMemberVoted) { - console.log(`❌ Member has already voted in this round`); - return res.status(400).json({ ok: false, message: 'Member has already voted' }); - } - - console.log(`âŦ Processing vote for ${member.name} in round - ${round.name}`); - - const votePromises = Object.entries(votes).map(async ([role, contestantIds]) => { - if (Array.isArray(contestantIds)) { - return Promise.all( - contestantIds.map(async (contestantId: string) => { - console.log(`âŦ Processing vote for role ${role} and contestant ID ${contestantId}`); - - const contestant = - contestantId.startsWith('00000000000000000000') // 20 times '0' for white vote as a white vote id is 24(last 4 characters are for id) - ? getWhiteVoteMember(contestantId) - : await db.getMember({ _id: new ObjectId(contestantId) }); - - if (!contestant) { - console.log(`❌ Contestant with ID ${contestantId} not found`); - return null; - } - - const vote = { - round: round._id, - role: role as Positions, - contestant: contestant._id - }; - - console.log('Vote:', JSON.stringify(vote)); - return db.addVote(vote); - }) - ); - } - return []; // Ensure a promise is always returned - }); + if (!member) { + console.log(`❌ Member with ID ${memberId} not found`); + return res.status(404).json({ ok: false, message: 'Member not found' }); + } - await Promise.all(votePromises.flat()); // flat might be needed if inner promises are nested - await db.markMemberVoted(round._id.toString(), member._id.toString(), signature); + const isLocked = await db.isRoundLocked(roundId); + if (isLocked) { + console.log(`❌ Round ${roundId} is locked`); + return res.status(400).json({ ok: false, message: 'Round is locked' }); + } - console.log('✅ Votes recorded successfully'); - return res.json({ ok: true }); - } catch (error) { - console.error('❌ Error processing vote:', error); - return res.status(500).json({ ok: false, message: 'Internal server error' }); + const hasMemberVoted = await db.hasMemberVoted(round._id.toString(), member._id.toString()); + if (hasMemberVoted) { + console.log(`❌ Member has already voted in this round`); + return res.status(400).json({ ok: false, message: 'Member has already voted' }); } + + console.log(`âŦ Processing vote for ${member.name} in round - ${round.name}`); + + const votePromises = Object.entries(votes).map(async ([role, contestantIds]) => { + if (Array.isArray(contestantIds)) { + return Promise.all( + contestantIds.map(async (contestantId: string) => { + console.log(`âŦ Processing vote for role ${role} and contestant ID ${contestantId}`); + const contestant = contestantId.startsWith('00000000000000000000') // 20 times '0' for white vote as a white vote id is 24(last 4 characters are for id) + ? getWhiteVoteMember(contestantId, round?.eventId?.toString()) + : await db.getMember({ _id: new ObjectId(contestantId) }); + + if (!contestant) { + console.log(`❌ Contestant with ID ${contestantId} not found`); + return null; + } + + const vote = { + round: round._id, + role: role as Positions, + contestant: contestant._id + }; + + console.log('Vote:', JSON.stringify(vote)); + return db.addVote(vote); + }) + ); + } + return []; // Ensure a promise is always returned + }); + + await Promise.all(votePromises.flat()); // flat might be needed if inner promises are nested + await db.markMemberVoted(round._id.toString(), member._id.toString(), signature); + + console.log('✅ Votes recorded successfully'); + return res.json({ ok: true }); + } catch (error) { + console.error('❌ Error processing vote:', error); + return res.status(500).json({ ok: false, message: 'Internal server error' }); + } }); export default router; diff --git a/apps/backend/src/routers/auth.ts b/apps/backend/src/routers/auth.ts index 7e78ac8..fa8ebfa 100644 --- a/apps/backend/src/routers/auth.ts +++ b/apps/backend/src/routers/auth.ts @@ -3,6 +3,7 @@ import dayjs from 'dayjs'; import jwt from 'jsonwebtoken'; import * as db from '@mtes/database'; import { User } from '@mtes/types'; +import { ObjectId } from 'mongodb'; const router = express.Router({ mergeParams: true }); @@ -10,6 +11,7 @@ const jwtSecret = process.env.JWT_SECRET; router.post('/login', async (req: Request, res: Response, next: NextFunction) => { const loginDetails: User = req.body; + if (loginDetails.eventId) loginDetails.eventId = new ObjectId(loginDetails.eventId); try { const user = await db.getUser({ ...loginDetails }); @@ -53,4 +55,4 @@ router.post('/logout', (req: Request, res: Response) => { return res.json({ ok: true }); }); -export default router; \ No newline at end of file +export default router; diff --git a/apps/backend/src/routers/public/index.ts b/apps/backend/src/routers/public/index.ts index 83610e2..638b1b6 100644 --- a/apps/backend/src/routers/public/index.ts +++ b/apps/backend/src/routers/public/index.ts @@ -1,12 +1,19 @@ import express, { Request, Response } from 'express'; import * as db from '@mtes/database'; +import { ObjectId } from 'mongodb'; const router = express.Router({ mergeParams: true }); router.get('/event', (req: Request, res: Response) => { - db.getElectionEvent().then(event => { + db.getElectionEvent(new ObjectId(req.params.eventId)).then(event => { res.status(200).json(event); }); }); +router.get('/events', (req: Request, res: Response) => { + db.getAllElectionEvents().then(events => { + res.status(200).json(events); + }); +}); + export default router; diff --git a/apps/backend/src/websocket/handlers.ts b/apps/backend/src/websocket/handlers.ts index d4d7719..fffa636 100644 --- a/apps/backend/src/websocket/handlers.ts +++ b/apps/backend/src/websocket/handlers.ts @@ -88,7 +88,6 @@ export const handleUpdateMemberPresence = async ( replacedBy: WithId | null, callback: ((response: { ok: boolean; error?: string }) => void) | undefined ) => { - if (!memberId) { console.log('❌ Member ID is null or undefined'); if (typeof callback === 'function') { @@ -102,7 +101,7 @@ export const handleUpdateMemberPresence = async ( const updatePayload: Partial = { isPresent, - replacedBy: replacedBy ? replacedBy as WithId : null + replacedBy: replacedBy ? (replacedBy as WithId) : null }; try { @@ -112,7 +111,9 @@ export const handleUpdateMemberPresence = async ( updatePayload as unknown as Partial ); if (!result.acknowledged || result.matchedCount === 0) { - console.log(`❌ Could not update presence for member ${memberId}. Member not found or update failed.`); + console.log( + `❌ Could not update presence for member ${memberId}. Member not found or update failed.` + ); if (typeof callback === 'function') { callback({ ok: false, error: 'Member not found or update failed' }); } @@ -125,7 +126,9 @@ export const handleUpdateMemberPresence = async ( updatePayload as unknown as Partial ); if (!result.acknowledged || result.matchedCount === 0) { - console.log(`❌ Could not update presence for member ${memberId}. Member not found or update failed.`); + console.log( + `❌ Could not update presence for member ${memberId}. Member not found or update failed.` + ); if (typeof callback === 'function') { callback({ ok: false, error: 'Member not found or update failed' }); } @@ -133,24 +136,23 @@ export const handleUpdateMemberPresence = async ( } console.log(`✅ Presence updated for member ${memberId}`); } - namespace.emit('memberPresenceUpdated', - memberId, - isMM, - isPresent, - replacedBy - ); - + namespace.emit('memberPresenceUpdated', memberId, isMM, isPresent, replacedBy); } catch (error) { console.error('❌ Error updating member presence:', error); if (typeof callback === 'function') { callback({ ok: false, error: 'Internal server error while updating member presence' }); } } -} +}; export const handleUpdateAudienceDisplay = async ( namespace: any, - view: { display: 'round' | 'presence' | 'voting' | 'member' | 'message'; round?: WithId; member?: WithId; message?: string }, + view: { + display: 'round' | 'presence' | 'voting' | 'member' | 'message'; + round?: WithId; + member?: WithId; + message?: string; + }, callback: ((response: { ok: boolean; error?: string }) => void) | undefined ) => { console.log(`🔌 WS: Update audience display to ${view}`); @@ -173,4 +175,4 @@ export const handleUpdateAudienceDisplay = async ( callback({ ok: false, error: 'Failed to update audience display' }); } } -} \ No newline at end of file +}; diff --git a/apps/backend/src/websocket/index.ts b/apps/backend/src/websocket/index.ts index e18a1ab..19de563 100644 --- a/apps/backend/src/websocket/index.ts +++ b/apps/backend/src/websocket/index.ts @@ -1,10 +1,13 @@ import { Namespace, Socket } from 'socket.io'; +import { WSServerEmittedEvents, WSClientEmittedEvents, WSInterServerEvents } from '@mtes/types'; import { - WSServerEmittedEvents, - WSClientEmittedEvents, - WSInterServerEvents, -} from '@mtes/types'; -import { handleLoadRound, handleLoadVotingMember, handleUpdateAudienceDisplay, handleUpdateMemberPresence, handleVoteProcessed, handleVoteSubmitted } from './handlers'; + handleLoadRound, + handleLoadVotingMember, + handleUpdateAudienceDisplay, + handleUpdateMemberPresence, + handleVoteProcessed, + handleVoteSubmitted +} from './handlers'; const websocket = ( socket: Socket @@ -18,7 +21,7 @@ const websocket = ( socket.on('updateMemberPresence', (...args) => { handleUpdateMemberPresence(namespace, ...args); - }) + }); socket.on('loadVotingMember', (...args) => handleLoadVotingMember(namespace, ...args)); @@ -28,13 +31,13 @@ const websocket = ( handleUpdateAudienceDisplay(namespace, view, callback); }); - socket.on('voteSubmitted', ((...args) => { + socket.on('voteSubmitted', (...args) => { handleVoteSubmitted(namespace, ...args); - })); + }); - socket.on('voteProcessed', ((...args) => { + socket.on('voteProcessed', (...args) => { handleVoteProcessed(namespace, ...args); - })); + }); socket.on('disconnect', () => { console.log(`❌ WS: Disconnection`); @@ -42,4 +45,3 @@ const websocket = ( }; export default websocket; - diff --git a/apps/frontend/components/admin/ChangePasswordDialog.tsx b/apps/frontend/components/admin/ChangePasswordDialog.tsx index f914061..e261d7a 100644 --- a/apps/frontend/components/admin/ChangePasswordDialog.tsx +++ b/apps/frontend/components/admin/ChangePasswordDialog.tsx @@ -97,8 +97,8 @@ const ChangePasswordDialog: React.FC = ({ open, onClo }; return ( - = ({ open, onClo שינוי סיסמ×Ē ×ž× ×”×œ - + {error && ( @@ -124,7 +124,7 @@ const ChangePasswordDialog: React.FC = ({ open, onClo label="סיסמה נוכחי×Ē" type={showCurrentPassword ? 'text' : 'password'} value={currentPassword} - onChange={(e) => setCurrentPassword(e.target.value)} + onChange={e => setCurrentPassword(e.target.value)} disabled={loading} InputProps={{ dir: 'ltr', @@ -147,7 +147,7 @@ const ChangePasswordDialog: React.FC = ({ open, onClo label="סיסמה חדשה" type={showNewPassword ? 'text' : 'password'} value={newPassword} - onChange={(e) => setNewPassword(e.target.value)} + onChange={e => setNewPassword(e.target.value)} disabled={loading} helperText="לפחו×Ē 4 ×Ēווים" InputProps={{ @@ -171,7 +171,7 @@ const ChangePasswordDialog: React.FC = ({ open, onClo label="אימו×Ē ×Ą×™×Ą×ž×” חדשה" type={showConfirmPassword ? 'text' : 'password'} value={confirmPassword} - onChange={(e) => setConfirmPassword(e.target.value)} + onChange={e => setConfirmPassword(e.target.value)} disabled={loading} InputProps={{ dir: 'ltr', @@ -192,18 +192,10 @@ const ChangePasswordDialog: React.FC = ({ open, onClo - - diff --git a/apps/frontend/components/connection-indicator.tsx b/apps/frontend/components/connection-indicator.tsx index 89568ab..7d08d33 100644 --- a/apps/frontend/components/connection-indicator.tsx +++ b/apps/frontend/components/connection-indicator.tsx @@ -13,26 +13,26 @@ const config: { rippleColor: '#3cd3b2', textColor: '#111111', backgroundColor: '#f4f4f4', - text: 'מחובר', + text: 'מחובר' }, connecting: { rippleColor: '#a21caf', textColor: '#000000', backgroundColor: '#f4f4f4', - text: 'מ×Ēחבר...', + text: 'מ×Ēחבר...' }, disconnected: { rippleColor: '#f87171', textColor: '#000000', backgroundColor: '#f4f4f4', - text: 'מנו×Ē×§', + text: 'מנו×Ē×§' }, error: { rippleColor: '#ffffff', textColor: '#ffffff', backgroundColor: '#dc2626', - text: 'שגיאה', - }, + text: 'שגיאה' + } } as const; const rippleAnimation = keyframes` @@ -45,9 +45,7 @@ interface ConnectionIndicatorProps { status: ConnectionStatus; } -const ConnectionIndicator: React.FC = ({ - status, -}) => { +const ConnectionIndicator: React.FC = ({ status }) => { const { rippleColor, textColor, backgroundColor, text } = config[status]; return ( = ({ fontSize: '0.875rem', fontWeight: 500, minWidth: 100, - transition: 'all 0.2s ease-in-out', + transition: 'all 0.2s ease-in-out' }} > = ({ boxShadow: `0 0 0 0.25rem ${rippleColor}33`, mr: 1.25, animation: `${rippleAnimation} 2s linear infinite`, - transition: 'all 0.2s ease-in-out', + transition: 'all 0.2s ease-in-out' }} /> diff --git a/apps/frontend/components/forms/event-selector.tsx b/apps/frontend/components/forms/event-selector.tsx index 7a87e61..b7a5f2d 100644 --- a/apps/frontend/components/forms/event-selector.tsx +++ b/apps/frontend/components/forms/event-selector.tsx @@ -4,6 +4,7 @@ import { ElectionEvent } from '@mtes/types'; import { WithId, ObjectId } from 'mongodb'; import { Avatar, ListItemAvatar, ListItemButton, ListItemText, List } from '@mui/material'; import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded'; +import HomeIcon from '@mui/icons-material/HomeRounded'; import EventIcon from '@mui/icons-material/EventOutlined'; import { stringifyTwoDates } from '../../lib/utils/dayjs'; import { getBackgroundColor } from '../../lib/utils/theme'; @@ -11,53 +12,38 @@ import { getBackgroundColor } from '../../lib/utils/theme'; interface EventSelectorProps { events: Array>; onChange: (eventId: string | ObjectId) => void; - getEventDisabled?: (event: WithId) => boolean; } -const EventSelector: React.FC = ({ events, onChange, getEventDisabled }) => { +const EventSelector: React.FC = ({ events, onChange }) => { const sortedEvents = useMemo( - () => - events.sort((a, b) => { - const diffA = dayjs().diff(dayjs(a.startDate), 'days', true); - const diffB = dayjs().diff(dayjs(b.startDate), 'days', true); - - if (diffB > 1 && diffA <= 1) return -1; - if (diffA > 1 && diffB <= 1) return 1; - if (diffA > 1 && diffB > 1) return diffA - diffB; - return diffB - diffA; - }), + () => [...events].sort((a, b) => a.name.localeCompare(b.name)), [events] ); return ( {sortedEvents.map(event => { - const disabled = getEventDisabled?.(event); - return ( - onChange(event._id)} - disabled={disabled} - sx={{ borderRadius: 2 }} - component="a" - dense - > - - - - - - - {disabled && } - + + onChange(event._id)} + sx={{ borderRadius: 2 }} + component="a" + dense + > + + + + + + + + ); })} diff --git a/apps/frontend/components/general/event-selector.tsx b/apps/frontend/components/general/event-selector.tsx index 5a147e6..6a08a86 100644 --- a/apps/frontend/components/general/event-selector.tsx +++ b/apps/frontend/components/general/event-selector.tsx @@ -15,11 +15,7 @@ interface EventSelectorProps { getEventDisabled?: (event: WithId) => boolean; } -const EventSelector: React.FC = ({ - events, - onChange, - getEventDisabled, -}) => { +const EventSelector: React.FC = ({ events, onChange, getEventDisabled }) => { const sortedEvents = useMemo( () => events.sort((a, b) => { @@ -37,8 +33,7 @@ const EventSelector: React.FC = ({ return ( {sortedEvents.map(event => { - const disabled = - getEventDisabled?.(event) + const disabled = getEventDisabled?.(event); return ( diff --git a/apps/frontend/components/general/login/admin-login-form.tsx b/apps/frontend/components/general/login/admin-login-form.tsx index 243d162..eea43c2 100644 --- a/apps/frontend/components/general/login/admin-login-form.tsx +++ b/apps/frontend/components/general/login/admin-login-form.tsx @@ -4,10 +4,13 @@ import { useSnackbar } from 'notistack'; import { Button, Box, Typography, Stack, TextField } from '@mui/material'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; import { apiFetch } from '../../../lib/utils/fetch'; +import { ObjectId } from 'mongodb'; -interface Props {} +interface Props { + eventId?: string | ObjectId; +} -const AdminLoginForm: React.FC = ({}) => { +const AdminLoginForm: React.FC = ({ eventId }) => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); @@ -22,9 +25,10 @@ const AdminLoginForm: React.FC = ({}) => { isAdmin: true, username, password, - }), + eventId: eventId ? String(eventId) : undefined + }) }) - .then(async (res) => { + .then(async res => { const data = await res.json(); if (data && !data.error) { document.getElementById('recaptcha-script')?.remove(); @@ -35,16 +39,14 @@ const AdminLoginForm: React.FC = ({}) => { enqueueSnackbar('אופס, הסיסמה שגויה.', { variant: 'error' }); } else { enqueueSnackbar('הגישה נדח×Ēה, נסו שני×Ē ×ž××•×—×¨ יו×Ēר.', { - variant: 'error', + variant: 'error' }); } } else { throw new Error(res.statusText); } }) - .catch(() => - enqueueSnackbar('אופס, החיבור לשר×Ē × ×›×Š×œ.', { variant: 'error' }) - ); + .catch(() => enqueueSnackbar('אופס, החיבור לשר×Ē × ×›×Š×œ.', { variant: 'error' })); }; const handleSubmit = async (e: React.FormEvent) => { @@ -54,12 +56,7 @@ const AdminLoginForm: React.FC = ({}) => { }; return ( - + ה×Ēחברו×Ē ×›×ž× ×”×œ @@ -69,7 +66,7 @@ const AdminLoginForm: React.FC = ({}) => { type="username" label="׊ם מ׊×Ēמ׊" value={username} - onChange={(e) => setUsername(e.target.value)} + onChange={e => setUsername(e.target.value)} fullWidth /> = ({}) => { type="password" label="סיסמה" value={password} - onChange={(e) => setPassword(e.target.value)} + onChange={e => setPassword(e.target.value)} inputProps={{ dir: 'ltr' }} /> diff --git a/apps/frontend/components/layout.tsx b/apps/frontend/components/layout.tsx index b9c0bff..aabbaaf 100644 --- a/apps/frontend/components/layout.tsx +++ b/apps/frontend/components/layout.tsx @@ -15,7 +15,7 @@ import { Stack, Toolbar, Tooltip, - Typography, + Typography } from '@mui/material'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import LogoutIcon from '@mui/icons-material/Logout'; @@ -44,7 +44,7 @@ const Layout: React.FC = ({ maxWidth = 'lg', action, error, - color, + color }) => { const router = useRouter(); const [open, setOpen] = useState(false); @@ -54,7 +54,7 @@ const Layout: React.FC = ({ const handleBack = () => { const queryString = router.query.divisionId ? new URLSearchParams({ - divisionId: router.query.divisionId as string, + divisionId: router.query.divisionId as string }).toString() : ''; const url = `${back}${queryString ? `?${queryString}` : ''}`; @@ -62,7 +62,7 @@ const Layout: React.FC = ({ }; const logout = () => { - apiFetch('/auth/logout', { method: 'POST' }).then(res => router.push('/')); + apiFetch('/auth/logout', { method: 'POST' }).then(res => router.push('/')); }; return ( @@ -73,7 +73,7 @@ const Layout: React.FC = ({ position="fixed" sx={{ // animation: isError ? `${errorAnimation} 1s linear infinite alternate` : undefined, - borderBottom: color && `5px solid ${color}`, + borderBottom: color && `5px solid ${color}` }} > @@ -102,9 +102,7 @@ const Layout: React.FC = ({ - {connectionStatus && ( - - )} + {connectionStatus && } {action} @@ -115,7 +113,7 @@ const Layout: React.FC = ({ - theme.mixins.toolbar.minHeight }} /> + theme.mixins.toolbar.minHeight }} /> )} = ({ {children} @@ -166,7 +160,7 @@ const Layout: React.FC = ({ bottom: '2rem', right: '12%', left: '12%', - borderRadius: 4, + borderRadius: 4 }} > שגיאה בל×Ēי ×Ļפויה, אנא פנו למנהל המ×ĸרכ×Ē. diff --git a/apps/frontend/components/login/division-login-form.tsx b/apps/frontend/components/login/division-login-form.tsx deleted file mode 100644 index e814d55..0000000 --- a/apps/frontend/components/login/division-login-form.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { useMemo, useState } from 'react'; -import { useRouter } from 'next/router'; -import { useSnackbar } from 'notistack'; -import { WithId } from 'mongodb'; -import { Button, Box, Typography, Stack, MenuItem, TextField } from '@mui/material'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; -import { Role, RoleTypes } from '@mtes/types'; -import FormDropdown from './form-dropdown'; -import { apiFetch } from '../../lib/utils/fetch'; -import { localizedRoles } from '../../localization/roles'; - -interface DivisionLoginFormProps { - votingStands: number; -} - -const DivisionLoginForm: React.FC = ({ votingStands }) => { - const [role, setRole] = useState('' as Role); - const [password, setPassword] = useState(''); - const [association, setAssociation] = useState(); - - const router = useRouter(); - const { enqueueSnackbar } = useSnackbar(); - - const login = () => { - apiFetch('/auth/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - isAdmin: false, - role, - password, - ...(association - ? { - roleAssociation: { - type: 'stand', - value: association - } - } - : undefined) - }) - }) - .then(async res => { - const data = await res.json(); - if (data && !data.error) { - const returnUrl = router.query.returnUrl || `/mtes`; - router.push(returnUrl as string); - } else if (data.error) { - if (data.error === 'INVALID_CREDENTIALS') { - enqueueSnackbar('אופס, הסיסמה שגויה.', { variant: 'error' }); - } else { - enqueueSnackbar('הגישה נדח×Ēה, נסו שני×Ē ×ž××•×—×¨ יו×Ēר.', { variant: 'error' }); - } - } else { - throw new Error(res.statusText); - } - }) - .catch(() => enqueueSnackbar('אופס, החיבור לשר×Ē × ×›×Š×œ.', { variant: 'error' })); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - login(); - }; - - return ( - - - ה×Ēחברו×Ē ×œ××™×¨×•×ĸ: - - - - { - setRole(e.target.value as Role); - }} - > - {RoleTypes.map((r: Role) => { - return ( - - {localizedRoles[r as Role]} - - ); - })} - - {role === 'voting-stand' && ( - setAssociation(e.target.value)} - > - {Array.from({ length: votingStands }, (_, i) => i + 1).map(stand => ( - - קלפי {stand} - - ))} - - )} - setPassword(e.target.value)} - slotProps={{ htmlInput: { dir: 'ltr' } }} - /> - - - - - - ); -}; - -export default DivisionLoginForm; diff --git a/apps/frontend/components/login/event-login-form.tsx b/apps/frontend/components/login/event-login-form.tsx index 1757a8d..ea43b93 100644 --- a/apps/frontend/components/login/event-login-form.tsx +++ b/apps/frontend/components/login/event-login-form.tsx @@ -1,47 +1,55 @@ -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { useRouter } from 'next/router'; import { useSnackbar } from 'notistack'; -import { WithId } from 'mongodb'; +import { WithId, ObjectId } from 'mongodb'; import { Button, Box, Typography, Stack, MenuItem, TextField } from '@mui/material'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; -import { Role, ElectionEvent } from '@mtes/types'; -// import FormDropdown from './form-dropdown'; +import { Role, RoleTypes } from '@mtes/types'; +import FormDropdown from './form-dropdown'; import { apiFetch } from '../../lib/utils/fetch'; import { localizedRoles } from '../../localization/roles'; -import FormDropdown from './form-dropdown'; -interface Props { - event: WithId; - onCancel: () => void; +interface DivisionLoginFormProps { + votingStands: number; + eventId?: string | ObjectId; + onCancel?: () => void; } -const EventLoginForm: React.FC = ({ event, onCancel }): JSX.Element => { +const DivisionLoginForm: React.FC = ({ + votingStands, + eventId, + onCancel +}) => { const [role, setRole] = useState('' as Role); const [password, setPassword] = useState(''); - - const loginRoles = Object.keys(event.eventUsers); + const [association, setAssociation] = useState(); const router = useRouter(); const { enqueueSnackbar } = useSnackbar(); - const login = (captchaToken?: string) => { + const login = () => { apiFetch('/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ isAdmin: false, - // eventId: event._id, role, password, - ...(captchaToken ? { captchaToken } : {}) + eventId: eventId ? String(eventId) : undefined, + ...(association + ? { + roleAssociation: { + type: 'stand', + value: association + } + } + : undefined) }) }) .then(async res => { const data = await res.json(); if (data && !data.error) { - document.getElementById('recaptcha-script')?.remove(); - document.querySelector('.grecaptcha-badge')?.remove(); const returnUrl = router.query.returnUrl || `/mtes`; router.push(returnUrl as string); } else if (data.error) { @@ -72,9 +80,7 @@ const EventLoginForm: React.FC = ({ event, onCancel }): JSX.Element => { ה×Ēחברו×Ē ×œ××™×¨×•×ĸ: - - {event.name} - + = ({ event, onCancel }): JSX.Element => { setRole(e.target.value as Role); }} > - {loginRoles - .filter((r): r is Role => r === 'election-manager' || r === 'voting-stand') - .map((r: Role) => { - return ( - - {localizedRoles[r]} - - ); - })} + {RoleTypes.map((r: Role) => { + return ( + + {localizedRoles[r as Role]} + + ); + })} - + {role === 'voting-stand' && ( + setAssociation(e.target.value)} + > + {Array.from({ length: votingStands }, (_, i) => i + 1).map(stand => ( + + קלפי {stand} + + ))} + + )} = ({ event, onCancel }): JSX.Element => { + + + ); + return ( +
+ + setCurrentTab(val)} + centered + sx={{ mb: 2 }} + > + } /> + } /> + } /> + + + + + {currentTab === 0 && } + {currentTab === 1 && ( + + + + {renderActionButtons()} + + )} + {currentTab === 2 && ( + + )} + {currentTab === 3 && ( + + + ניהול מ׊×Ēמשים + + + {renderActionButtons()} + + )} + {currentTab === 4 && ( + + + הגדרו×Ē ×ž× ×”×œ + + + + + שינוי סיסמה + + + ×ĸדכן א×Ē ×Ą×™×Ą×ž×Ē ×”×ž× ×”×œ שלך למטרו×Ē ××‘×˜×—×” + + + + + {renderActionButtons()} + + )} + + ); + }} + + + setPasswordDialogOpen(false)} + /> + setDeleteConfirmOpen(false)} + maxWidth="sm" + fullWidth + > + אישור מחיק×Ē ××™×¨×•×ĸ + + האם א×Ēה בטוח שבר×Ļונך למחוק א×Ē ×”××™×¨×•×ĸ? פ×ĸולה זו אינה ני×Ē× ×Ē ×œ×‘×™×˜×•×œ. + + + + + + + + ); +}; + +export const getServerSideProps: GetServerSideProps = async ( + ctx: GetServerSidePropsContext +) => { + const eventId = ctx.params?.eventId; + const data = await serverSideGetRequests( + { + user: '/api/me', + event: `/api/events/${eventId}`, + initMembers: `/api/events/${eventId}/members`, + initMMMembers: `/api/events/${eventId}/mm-members`, + initCities: `/api/admin/events/${eventId}/cities`, + credentials: `/api/admin/events/${eventId}/users/credentials` + }, + ctx + ); + + console.log('event : ', data.event); + + return { + props: { + user: data.user, + event: data.event ?? null, + initMembers: data.initMembers ?? [], + initMMMembers: data.initMMMembers ?? [], + initCities: data.initCities ?? [], + credentials: data.credentials ?? [] + } + }; +}; + +export default Page; diff --git a/apps/frontend/pages/admin/create.tsx b/apps/frontend/pages/admin/create.tsx new file mode 100644 index 0000000..da3b1d3 --- /dev/null +++ b/apps/frontend/pages/admin/create.tsx @@ -0,0 +1,272 @@ +import { GetServerSideProps, NextPage } from 'next'; +import type { GetServerSidePropsContext } from 'next'; +import { useRouter } from 'next/router'; +import React, { useState } from 'react'; +import { + Paper, + Typography, + Stack, + Button, + Box, + Tabs, + Tab, + Dialog, + DialogTitle, + DialogContent, + DialogActions +} from '@mui/material'; +import ErrorIcon from '@mui/icons-material/Error'; +import { enqueueSnackbar } from 'notistack'; +import { Formik, Form, FormikHelpers, getIn } from 'formik'; +import { z } from 'zod'; +import { toFormikValidationSchema } from 'zod-formik-adapter'; +import type { WithId } from 'mongodb'; +import type { ElectionEvent, Member, User, City } from '@mtes/types'; +import { apiFetch, getUserAndDivision, serverSideGetRequests } from '../../lib/utils/fetch'; +import Layout from '../../components/layout'; +import UsersTable from '../../components/admin/users-table'; +import EventDetailsForm from '../../components/admin/EventDetailsForm'; +import MembersManagementForm from '../../components/admin/MembersManagementForm'; +import CitiesManagementForm from '../../components/admin/CitiesManagementForm'; +import ChangePasswordDialog from '../../components/admin/ChangePasswordDialog'; + +const memberFormSchema = z.object({ + _id: z.string().optional(), + name: z.string().min(1, '׊ם החבר הוא שדה חובה'), + city: z.string().min(1, 'יש לבחור מוסד שולח לחבר'), + isPresent: z.boolean().optional().default(false) +}); + +const createValidationSchema = (isNewEvent: boolean) => + z + .object({ + name: z.string().min(1, '׊ם האירו×ĸ הוא שדה חובה'), + votingStands: z.coerce + .number({ required_error: 'מספר ×ĸמדו×Ē ×”×Ļב×ĸה הוא שדה חובה' }) + .min(1, 'לפחו×Ē ×ĸמד×Ē ×”×Ļב×ĸה אח×Ē × ×“×¨×Š×Ē'), + electionThreshold: z.coerce + .number({ required_error: 'אחוז הכשירו×Ē ×”×•× שדה חובה' }) + .min(0, 'אחוז הכשירו×Ē ×—×™×™×‘ להיו×Ē ×œ×¤×—×•×Ē 0') + .max(100, 'אחוז הכשירו×Ē ×œ× יכול להיו×Ē ×™×•×Ēר מ-100'), + cities: z.array( + z.object({ + name: z.string().min(1, '׊ם המוסד השולח לא יכול להיו×Ē ×¨×™×§'), + numOfVoters: z.coerce.number().min(0, 'מספר המ×Ļבי×ĸים חייב להיו×Ē ×œ×¤×—×•×Ē 0') + }) + ), + regularMembers: z.array(memberFormSchema), + mmMembers: z.array(memberFormSchema) + }) + .refine(data => !isNewEvent || data.regularMembers.length + data.mmMembers.length > 0, { + message: 'לי×Ļיר×Ē ××™×¨×•×ĸ חדש, יש להזין לפחו×Ē ×—×‘×¨ אחד (× ×Ļיג או מ"מ)', + path: ['regularMembers'] + }) + .refine( + data => { + const allMembers = [...data.regularMembers, ...data.mmMembers]; + return allMembers.every(member => data.cities.some(city => city.name === member.city)); + }, + { + message: 'חבר אחד או יו×Ēר משויך למוסד שולח שאינה קיימ×Ē ×‘×¨×Š×™×ž×”', + path: ['regularMembers'] + } + ) + .refine( + data => { + const cityConfigs = data.cities.reduce((acc, city) => { + acc[city.name] = city.numOfVoters; + return acc; + }, {} as Record); + + for (const city of data.cities) { + const regularMembersInCityCount = data.regularMembers.filter( + m => m.city === city.name + ).length; + if (regularMembersInCityCount > (cityConfigs[city.name] || 0)) { + return false; + } + } + return true; + }, + { + message: + 'מספר הנ×Ļיגים במוסד השולח אינו יכול ל×ĸלו×Ē ×ĸל מספר המ×Ļבי×ĸים שהוגדר לאו×Ēה מוסד שולח. יש לה×ĸביר חברים ×ĸודפים לרשימ×Ē ×ž×ž×œ××™ מקום.', + path: ['regularMembers'] + } + ); + +export type FormValues = z.infer>; + +const Page: NextPage = () => { + const router = useRouter(); + const [currentTab, setCurrentTab] = useState(0); + const [passwordDialogOpen, setPasswordDialogOpen] = useState(false); + + const validationSchema = createValidationSchema(true); + + const initialValues: FormValues = { + name: '', + votingStands: 1, + electionThreshold: 50, + regularMembers: [], + mmMembers: [], + cities: [] + }; + + const handleSubmit = async (values: FormValues, { setSubmitting }: FormikHelpers) => { + setSubmitting(true); + const updateEndpoint = async ( + endpoint: string, + method: 'POST' | 'PUT', + body: unknown, + entityName: string + ) => { + const res = await apiFetch(endpoint, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + if (!res.ok) { + const errorData = await res.json().catch(() => null); + throw new Error(errorData?.message || `An error occurred while updating ${entityName}.`); + } + return res.json(); + }; + try { + const eventDetailsPayload = { + name: values.name, + votingStands: values.votingStands, + electionThreshold: values.electionThreshold + }; + const eventUpdateRes = await updateEndpoint( + '/api/admin/events', + 'POST', + eventDetailsPayload, + 'event details' + ); + const eventId = eventUpdateRes.id as string; + console.log(`Event created with ID: ${eventId}`); + + const regularMembersPayload = values.regularMembers.map(m => ({ + ...m, + isMM: false, + isPresent: m.isPresent || false, + eventId + })); + const mmMembersPayload = values.mmMembers.map(m => ({ + ...m, + isMM: true, + isPresent: m.isPresent || false, + eventId + })); + await updateEndpoint( + `/api/events/${eventId}/members`, + 'PUT', + { members: regularMembersPayload }, + 'members' + ); + if (mmMembersPayload.length > 0) { + await updateEndpoint( + `/api/events/${eventId}/mm-members`, + 'PUT', + { mmMembers: mmMembersPayload }, + 'MM members' + ); + } + await updateEndpoint( + `/api/admin/events/${eventId}/cities`, + 'PUT', + { cities: values.cities as City[] }, + 'cities' + ); + enqueueSnackbar('האירו×ĸ נו×Ļר בה×Ļלחה', { variant: 'success' }); + router.push('/admin'); + } catch (error: any) { + enqueueSnackbar(error.message || 'אופס, איר×ĸה שגיאה בל×Ēי ×Ļפויה.', { + variant: 'error' + }); + } finally { + setSubmitting(false); + } + }; + + return ( + + + + י×Ļיר×Ē ××™×¨×•×ĸ חדש + + + {({ values, errors, touched, isSubmitting, setFieldValue }) => ( +
+ + setCurrentTab(val)} + centered + sx={{ mb: 2 }} + > + + + + + + {currentTab === 0 && <>} />} + {currentTab === 1 && ( + <>} + isNewEvent={true} + initCities={values.cities} + /> + )} + {currentTab === 2 && ( + + + + + )} + + + + + )} +
+
+ setPasswordDialogOpen(false)} + /> +
+ ); +}; + +export default Page; diff --git a/apps/frontend/pages/admin/index.tsx b/apps/frontend/pages/admin/index.tsx index 336d849..6351ce0 100644 --- a/apps/frontend/pages/admin/index.tsx +++ b/apps/frontend/pages/admin/index.tsx @@ -1,24 +1,22 @@ -import { useState } from 'react'; +import { useRouter } from 'next/router'; import { GetServerSideProps, NextPage } from 'next'; import type { GetServerSidePropsContext } from 'next'; -import { useRouter } from 'next/router'; -import { Paper, Typography, Stack, Button, Box, Tabs, Tab, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material'; -import ErrorIcon from '@mui/icons-material/Error'; -import { enqueueSnackbar } from 'notistack'; -import { Formik, Form, FormikHelpers, getIn } from 'formik'; -import { z } from 'zod'; -import { toFormikValidationSchema } from 'zod-formik-adapter'; - -import type { WithId } from 'mongodb'; -import type { ElectionEvent, Member, User, City } from '@mtes/types'; - -import { apiFetch, serverSideGetRequests } from '../../lib/utils/fetch'; +import { + Paper, + Typography, + Stack, + Button, + Box, + List, + ListItem, + ListItemButton, + ListItemText +} from '@mui/material'; import Layout from '../../components/layout'; -import UsersTable from '../../components/admin/users-table'; -import EventDetailsForm from '../../components/admin/EventDetailsForm'; -import MembersManagementForm from '../../components/admin/MembersManagementForm'; -import CitiesManagementForm from '../../components/admin/CitiesManagementForm'; -import ChangePasswordDialog from '../../components/admin/ChangePasswordDialog'; +import type { WithId } from 'mongodb'; +import type { ElectionEvent, User } from '@mtes/types'; +import { getUserAndDivision, serverSideGetRequests } from '../../lib/utils/fetch'; +import z from 'zod'; const memberFormSchema = z.object({ _id: z.string().optional(), @@ -85,352 +83,52 @@ const createValidationSchema = (isNewEvent: boolean) => } ); -export type FormValues = z.infer>; - export interface PageProps { user: WithId; - event?: WithId; - initMembers: WithId[]; // These are regular members (isMM: false or undefined) - initMMMembers: WithId[]; // These are MM members (isMM: true) - initCities: City[]; - credentials: User[]; + events: WithId[]; } -const Page: NextPage = ({ - user, - event, - initMembers, - initMMMembers, - initCities, - credentials -}) => { +const Page: NextPage = ({ user, events }) => { const router = useRouter(); - const [currentTab, setCurrentTab] = useState(0); - const [passwordDialogOpen, setPasswordDialogOpen] = useState(false); - const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); - - const isNewEvent = !event; - const validationSchema = createValidationSchema(isNewEvent); - - const initialValues: FormValues = { - name: event?.name || '', - votingStands: event?.votingStands || 1, - electionThreshold: event?.electionThreshold || 50, - regularMembers: Array.isArray(initMembers) - ? initMembers - .filter(m => !m.isMM) // Ensure only non-MM members (isMM is false or undefined) - .map(m => ({ - _id: m._id.toString(), - name: m.name, - city: m.city, - isPresent: m.isPresent || false - })) - : [], - mmMembers: Array.isArray(initMMMembers) - ? initMMMembers - .filter(m => m.isMM === true) // Ensure only explicitly MM members - .map(m => ({ - _id: m._id.toString(), - name: m.name, - city: m.city, - isPresent: m.isPresent || false - })) - : [], - cities: Array.isArray(initCities) ? initCities : [] - }; - - const handleSubmit = async (values: FormValues, { setSubmitting }: FormikHelpers) => { - setSubmitting(true); - const updateEndpoint = async ( - endpoint: string, - method: 'POST' | 'PUT', - body: unknown, - entityName: string - ) => { - const res = await apiFetch(endpoint, { - method, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body) - }); - - if (!res.ok) { - const errorData = await res.json().catch(() => null); - throw new Error(errorData?.message || `An error occurred while updating ${entityName}.`); - } - return res.json(); - }; - - try { - const eventDetailsPayload = { - name: values.name, - votingStands: values.votingStands, - electionThreshold: values.electionThreshold - }; - await updateEndpoint( - '/api/admin/events', - event ? 'PUT' : 'POST', - eventDetailsPayload, - 'event details' - ); - - const regularMembersPayload = values.regularMembers.map(m => ({ - ...m, - isMM: false, - isPresent: m.isPresent || false - })); - const mmMembersPayload = values.mmMembers.map(m => ({ - ...m, - isMM: true, - isPresent: m.isPresent || false - })); - - await updateEndpoint( - '/api/events/members', - 'PUT', - { members: regularMembersPayload }, - 'members' - ); - - // Send MM members to a different endpoint - if (mmMembersPayload.length > 0 || event) { - // Always send MM members if event exists, even if empty to clear them - await updateEndpoint( - '/api/events/mm-members', - 'PUT', - { mmMembers: mmMembersPayload }, - 'MM members' - ); - } - - await updateEndpoint( - '/api/admin/events/cities', - 'PUT', - { cities: values.cities as City[] }, - 'cities' - ); - - enqueueSnackbar('האירו×ĸ נ׊מר בה×Ļלחה', { variant: 'success' }); - router.reload(); - } catch (error: any) { - enqueueSnackbar(error.message || 'אופס, איר×ĸה שגיאה בל×Ēי ×Ļפויה.', { - variant: 'error' - }); - } finally { - setSubmitting(false); - } + const handleEventClick = (eventId: string) => { + router.push(`/admin/${eventId}`); }; - const handleDeleteClick = () => { - setDeleteConfirmOpen(true); + const handleCreateClick = () => { + router.push('/admin/create'); }; - const handleDeleteConfirm = async () => { - if (!event?._id) return; - - setDeleteConfirmOpen(false); - - try { - const res = await apiFetch(`/api/admin/events/data`, { - method: 'DELETE' - }); - - if (!res.ok) { - const errorData = await res.json().catch(() => null); - throw new Error(errorData?.message || 'An error occurred while deleting the event.'); - } - - enqueueSnackbar('האירו×ĸ נמחק בה×Ļלחה', { variant: 'success' }); - router.push('/admin'); - } catch (error: any) { - enqueueSnackbar(error.message || 'אופס, איר×ĸה שגיאה בל×Ēי ×Ļפויה.', { variant: 'error' }); - } - }; - - const TabLabel = ({ label, hasError }: { label: string; hasError: boolean }) => ( - - {label} - {hasError && } - - ); - return ( - + - {event ? '×ĸריכ×Ē ××™×¨×•×ĸ' : 'י×Ļיר×Ē ××™×¨×•×ĸ חדש'} + בחר אירו×ĸ לניהול - - {({ values, errors, touched, isSubmitting, setFieldValue }) => { - const hasEventDetailsError = !!( - (errors.name && touched.name) || - (errors.votingStands && touched.votingStands) || - (errors.electionThreshold && touched.electionThreshold) - ); - const hasMembersError = !!( - (getIn(errors, 'regularMembers') && getIn(touched, 'regularMembers')) || - (getIn(errors, 'mmMembers') && getIn(touched, 'mmMembers')) - ); - const hasCitiesError = !!(errors.cities && touched.cities); - - const renderActionButtons = () => ( - - - {event && ( - - )} - - ); - - return ( -
- - setCurrentTab(val)} - centered - sx={{ mb: 2 }} - > - } /> - } /> - } /> - - - - - - {currentTab === 0 && } - - {currentTab === 1 && ( - - - - {/* Render action buttons once after both member forms */} - {renderActionButtons()} - - )} - - {currentTab === 2 && ( - - )} - - {currentTab === 3 && ( - - - ניהול מ׊×Ēמשים - - - {renderActionButtons()} - - )} - - {currentTab === 4 && ( - - - הגדרו×Ē ×ž× ×”×œ - - - - - שינוי סיסמה - - - ×ĸדכן א×Ē ×Ą×™×Ą×ž×Ē ×”×ž× ×”×œ שלך למטרו×Ē ××‘×˜×—×” - - - - - {renderActionButtons()} - - )} - - ); - }} -
-
- - setPasswordDialogOpen(false)} - /> - - setDeleteConfirmOpen(false)} - maxWidth="sm" - fullWidth - > - - אישור מחיק×Ē ××™×¨×•×ĸ - - - - האם א×Ēה בטוח שבר×Ļונך למחוק א×Ē ×”××™×¨×•×ĸ? פ×ĸולה זו אינה ני×Ē× ×Ē ×œ×‘×™×˜×•×œ. - - - - - - - + + + אירו×ĸים קיימים: + + {events.length === 0 ? ( + לא נמ×Ļאו אירו×ĸים. + ) : ( + + {events.map(event => ( + + handleEventClick(event._id.toString())}> + + + + ))} + + )} + +
+ ); }; @@ -438,26 +136,18 @@ const Page: NextPage = ({ export const getServerSideProps: GetServerSideProps = async ( ctx: GetServerSidePropsContext ) => { + const { user } = await getUserAndDivision(ctx); const data = await serverSideGetRequests( { user: '/api/me', - event: '/public/event', - initMembers: '/api/events/members', // Fetches non-MM members - initMMMembers: '/api/events/mm-members', // Fetches MM members - initCities: '/api/admin/events/cities', - credentials: '/api/admin/events/users/credentials' + events: '/public/events' }, ctx ); - return { props: { user: data.user, - event: data.event ?? null, - initMembers: data.initMembers ?? [], - initMMMembers: data.initMMMembers ?? [], // Ensure this is correctly populated - initCities: data.initCities ?? [], - credentials: data.credentials ?? [] + events: data.events ?? [] } }; }; diff --git a/apps/frontend/pages/login.tsx b/apps/frontend/pages/login.tsx index 5f657b1..c12ab6c 100644 --- a/apps/frontend/pages/login.tsx +++ b/apps/frontend/pages/login.tsx @@ -5,22 +5,59 @@ import Layout from '../components/layout'; import AdminLoginForm from '../components/general/login/admin-login-form'; import { ElectionEvent, User } from '@mtes/types'; import { apiFetch, serverSideGetRequests } from '../lib/utils/fetch'; -import DivisionLoginForm from '../components/login/division-login-form'; +import DivisionLoginForm from '../components/login/event-login-form'; +import EventSelector from '../components/forms/event-selector'; +import { ObjectId, WithId } from 'mongodb'; interface LoginProps { - event?: ElectionEvent; + events: WithId[]; + defaultEvent?: WithId; } -const Page: NextPage = ({ event }) => { +const Page: NextPage = ({ events, defaultEvent }) => { const [isAdminLogin, setIsAdminLogin] = useState(false); + const [event, setEvent] = useState | null>(null); + + const handleEventSelect = (eventId: string | ObjectId) => { + const event = events.find(e => String(e._id) === String(eventId)); + if (event) { + setEvent(event); + } + }; + + // if (!events || events.length === 0) { + // return ( + // + // + // + // אין אירו×ĸים זמינים + // + // + // אנא פנה למנהל המ×ĸרכ×Ē + // + // + // + // ); + // } return ( - + {isAdminLogin ? ( + ) : event ? ( + setEvent(null)} + /> ) : ( - + + + בחיר×Ē ××™×¨×•×ĸ + + + )} @@ -50,19 +87,44 @@ export const getServerSideProps: GetServerSideProps = async ctx => { return response.ok ? response.json() : undefined; }); - const data = await serverSideGetRequests( - { - event: '/public/event' - }, - ctx - ); + try { + const data = await serverSideGetRequests( + { + events: '/public/events', + defaultEvent: '/public/event' + }, + ctx + ); + + if (user) { + return user.isAdmin + ? { redirect: { destination: `/admin`, permanent: false } } + : { redirect: { destination: `/mtes`, permanent: false } }; + } else { + // Ensure events is always an array + const events = Array.isArray(data.events) ? data.events : []; + return { + props: { + events, + defaultEvent: data.defaultEvent + } + }; + } + } catch (error) { + console.error('Error fetching events:', error); - if (user) { - return user.isAdmin - ? { redirect: { destination: `/admin`, permanent: false } } - : { redirect: { destination: `/mtes`, permanent: false } }; - } else { - return { props: { ...data } }; + if (user) { + return user.isAdmin + ? { redirect: { destination: `/admin`, permanent: false } } + : { redirect: { destination: `/mtes`, permanent: false } }; + } else { + return { + props: { + events: [], + defaultEvent: null + } + }; + } } }; diff --git a/apps/frontend/pages/mtes/audience-display.tsx b/apps/frontend/pages/mtes/audience-display.tsx index bf5a7d0..64ed16f 100644 --- a/apps/frontend/pages/mtes/audience-display.tsx +++ b/apps/frontend/pages/mtes/audience-display.tsx @@ -14,7 +14,7 @@ import { } from '@mtes/types'; import { WaitingState } from 'apps/frontend/components/mtes/waiting-state'; import { useWebsocket } from 'apps/frontend/hooks/use-websocket'; -import { apiFetch, getUserAndDivision, serverSideGetRequests } from 'apps/frontend/lib/utils/fetch'; +import { apiFetch, getUserAndDivision, serverSideGetRequests } from '../../lib/utils/fetch'; import { Box, Typography, Grid, Paper, Container, Avatar } from '@mui/material'; import Layout from '../../components/layout'; import { AudiencePresence } from 'apps/frontend/components/mtes/audience/audience-presence'; @@ -68,7 +68,7 @@ const Page: NextPage = ({ user, event, electionState, initialMembers, rou const refreshVotedMembers = async (roundId: string) => { console.log(`Fetching voted members for round ID: ${roundId}`); - const response = await apiFetch(`/api/events/rounds/votedMembers/${roundId}`, { + const response = await apiFetch(`/api/events/${event._id}/rounds/votedMembers/${roundId}`, { method: 'GET' }); if (response.ok) { @@ -228,14 +228,14 @@ const Page: NextPage = ({ user, event, electionState, initialMembers, rou export const getServerSideProps: GetServerSideProps = async ctx => { try { - const { user } = await getUserAndDivision(ctx); + const { user, eventId } = await getUserAndDivision(ctx); const data = await serverSideGetRequests( { - event: `/public/event`, - electionState: `/api/events/state`, - initialMembers: `/api/events/members`, - rounds: '/api/events/rounds' + event: `/api/events/${eventId}`, + electionState: `/api/events/${eventId}/state`, + initialMembers: `/api/events/${eventId}/members`, + rounds: `/api/events/${eventId}/rounds` }, ctx ); diff --git a/apps/frontend/pages/mtes/election-manager.tsx b/apps/frontend/pages/mtes/election-manager.tsx index b423d44..cb774fc 100644 --- a/apps/frontend/pages/mtes/election-manager.tsx +++ b/apps/frontend/pages/mtes/election-manager.tsx @@ -54,7 +54,7 @@ interface Props { mmMembers: WithId[]; rounds: WithId[]; electionState: WithId; - event: ElectionEvent; + event: WithId; eventState: WithId; } @@ -573,8 +573,6 @@ const Page: NextPage = ({ setSelectStandId(null); }; - console.log(votedMembers); - return ( = ({ handleShowResults={handleShowResults} members={members} refreshData={() => router.replace(router.asPath)} + eventId={event._id.toString()} />
)} @@ -841,20 +840,22 @@ const Page: NextPage = ({ export const getServerSideProps: GetServerSideProps = async ctx => { try { - const { user } = await getUserAndDivision(ctx); + const { user, eventId } = await getUserAndDivision(ctx); const data = await serverSideGetRequests( { - rounds: '/api/events/rounds', - electionState: '/api/events/state', - members: '/api/events/members', - mmMembers: '/api/events/mm-members', // Added mmMembers endpoint - event: '/public/event', - eventState: '/api/events/state' + rounds: `/api/events/${eventId}/rounds`, + event: `/api/events/${eventId}`, + electionState: `/api/events/${eventId}/state`, + members: `/api/events/${eventId}/members`, + mmMembers: `/api/events/${eventId}/mm-members`, + eventState: `/api/events/${eventId}/state` }, ctx ); + console.log('Server-side data fetched:', data); + return { props: { user, ...data } }; // Pass combined list as members } catch { return { redirect: { destination: '/login', permanent: false } }; diff --git a/apps/frontend/pages/mtes/index.tsx b/apps/frontend/pages/mtes/index.tsx index 9aaea98..03642e0 100644 --- a/apps/frontend/pages/mtes/index.tsx +++ b/apps/frontend/pages/mtes/index.tsx @@ -17,4 +17,4 @@ export const getServerSideProps: GetServerSideProps = async ctx => { }; }; -export default Page; \ No newline at end of file +export default Page; diff --git a/apps/frontend/pages/mtes/voting-stand.tsx b/apps/frontend/pages/mtes/voting-stand.tsx index 06148fb..b73192f 100644 --- a/apps/frontend/pages/mtes/voting-stand.tsx +++ b/apps/frontend/pages/mtes/voting-stand.tsx @@ -143,11 +143,11 @@ const Page: NextPage = ({ user, electionState }) => { export const getServerSideProps: GetServerSideProps = async ctx => { try { - const { user } = await getUserAndDivision(ctx); + const { user, eventId } = await getUserAndDivision(ctx); const data = await serverSideGetRequests( { - electionState: '/api/events/state' + electionState: `/api/events/${eventId}/state` }, ctx ); diff --git a/apps/frontend/tsconfig.json b/apps/frontend/tsconfig.json index 46c6b7e..676e452 100644 --- a/apps/frontend/tsconfig.json +++ b/apps/frontend/tsconfig.json @@ -16,7 +16,7 @@ { "name": "next" } - ], + ] }, "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts"], "exclude": ["node_modules", "src/**/*.spec.ts", "src/**/*.test.ts"] diff --git a/libs/database/src/lib/crud/cities.ts b/libs/database/src/lib/crud/cities.ts index 27cdb20..a5f8c01 100644 --- a/libs/database/src/lib/crud/cities.ts +++ b/libs/database/src/lib/crud/cities.ts @@ -3,30 +3,29 @@ import { City } from '@mtes/types'; import db from '../database'; export const getCities = (filter: Filter) => { - return db.collection('cities').find(filter).toArray(); + return db.collection('cities').find(filter).toArray(); }; export const getCity = (filter: Filter) => { - return db.collection('cities').findOne(filter); + return db.collection('cities').findOne(filter); }; export const addCity = (city: City) => { - return db.collection('cities').insertOne(city); + return db.collection('cities').insertOne(city); }; export const addCities = (cities: City[]) => { - return db.collection('cities').insertMany(cities); -} - + return db.collection('cities').insertMany(cities); +}; export const updateCity = (filter: Filter, newCity: Partial, upsert = false) => { - return db.collection('cities').updateOne(filter, { $set: newCity }, { upsert }); + return db.collection('cities').updateOne(filter, { $set: newCity }, { upsert }); }; export const deleteCity = (filter: Filter) => { - return db.collection('cities').deleteOne(filter); + return db.collection('cities').deleteOne(filter); }; -export const deleteCities = () => { - return db.collection('cities').deleteMany({}); +export const deleteCities = (filter: Filter) => { + return db.collection('cities').deleteMany(filter); }; diff --git a/libs/database/src/lib/crud/contestants.ts b/libs/database/src/lib/crud/contestants.ts index 32cd483..c611fe5 100644 --- a/libs/database/src/lib/crud/contestants.ts +++ b/libs/database/src/lib/crud/contestants.ts @@ -18,14 +18,20 @@ export const addContestants = (contestants: Array) => { return db.collection('contestants').insertMany(contestants); }; -export const updateContestant = (filter: Filter, newContestant: Partial, upsert = false) => { - return db.collection('contestants').updateOne(filter, { $set: newContestant }, { upsert }); +export const updateContestant = ( + filter: Filter, + newContestant: Partial, + upsert = false +) => { + return db + .collection('contestants') + .updateOne(filter, { $set: newContestant }, { upsert }); }; export const deleteContestant = (filter: Filter) => { return db.collection('contestants').deleteOne(filter); }; -export const deleteContestants = () => { - return db.collection('contestants').deleteMany(); +export const deleteContestants = (filter: Filter) => { + return db.collection('contestants').deleteMany(filter); }; diff --git a/libs/database/src/lib/crud/election-events.ts b/libs/database/src/lib/crud/election-events.ts index 7276cbf..6c4c800 100644 --- a/libs/database/src/lib/crud/election-events.ts +++ b/libs/database/src/lib/crud/election-events.ts @@ -1,9 +1,9 @@ -import { WithId, AggregationCursor, Filter } from 'mongodb'; +import { WithId, AggregationCursor, Filter, ObjectId } from 'mongodb'; import { ElectionEvent } from '@mtes/types'; import db from '../database'; -export const getElectionEvent = () => { - return findElectionEvents({}).next(); +export const getElectionEvent = (eventId: ObjectId) => { + return findElectionEvents({ _id: eventId }).next(); }; export const findElectionEvents = (filter: Filter) => { @@ -42,6 +42,8 @@ export const addElectionEvent = (ElectionEvent: ElectionEvent) => { return db.collection('election-events').insertOne(ElectionEvent); }; -export const deleteElectionEvent = () => { - return db.collection('election-events').drop(); +export const deleteElectionEvent = (eventId: ObjectId) => { + return db.collection('election-events').deleteOne({ + _id: eventId + }); }; diff --git a/libs/database/src/lib/crud/election-states.ts b/libs/database/src/lib/crud/election-states.ts index 4e03ef9..4bc1c19 100644 --- a/libs/database/src/lib/crud/election-states.ts +++ b/libs/database/src/lib/crud/election-states.ts @@ -1,8 +1,9 @@ import { ElectionState } from '@mtes/types'; import db from '../database'; +import { Filter, ObjectId } from 'mongodb'; -export const getElectionState = () => { - return db.collection('election-state').findOne(); +export const getElectionState = (filter: Filter) => { + return db.collection('election-state').findOne(filter); }; export const addElectionState = (state: ElectionState) => { @@ -17,6 +18,8 @@ export const updateElectionState = (newElectionState: Partial, up .updateOne({}, { $set: newElectionState }, { upsert }); }; -export const deleteElectionState = () => { - return db.collection('election-state').deleteOne({}); +export const deleteElectionState = (eventId: ObjectId) => { + return db.collection('election-state').deleteOne({ + _id: eventId + }); }; diff --git a/libs/database/src/lib/crud/members.ts b/libs/database/src/lib/crud/members.ts index b7352bc..fd7fc05 100644 --- a/libs/database/src/lib/crud/members.ts +++ b/libs/database/src/lib/crud/members.ts @@ -17,13 +17,14 @@ export const addMember = (team: Member) => { export const addMembers = (members: Array) => { const validMembers = members.map(member => { return { + eventId: member.eventId, name: member.name, city: member.city, isPresent: member.isPresent ?? false, + replacedBy: member.replacedBy ?? null, isMM: member.isMM ?? false }; - } - ) + }); return db.collection('members').insertMany(validMembers); }; diff --git a/libs/database/src/lib/crud/mm-members.ts b/libs/database/src/lib/crud/mm-members.ts index 6e93fdc..7aba94b 100644 --- a/libs/database/src/lib/crud/mm-members.ts +++ b/libs/database/src/lib/crud/mm-members.ts @@ -3,41 +3,43 @@ import { Member } from '@mtes/types'; import db from '../database'; export const getMmMember = (filter: Filter) => { - return db.collection('mm-members').findOne(filter); + return db.collection('mm-members').findOne(filter); }; export const getMmMembers = (filter: Filter) => { - return db.collection('mm-members').find(filter).toArray(); + return db.collection('mm-members').find(filter).toArray(); }; export const addMmMember = (mmMember: Member) => { - return db.collection('mm-members').insertOne(mmMember); + return db.collection('mm-members').insertOne(mmMember); }; export const addMmMembers = (mmMembers: Array) => { - const validMmMembers = mmMembers.map(mmMember => { - return { - name: mmMember.name, - city: mmMember.city, - isPresent: mmMember.isPresent ?? false, - isMM: mmMember.isMM ?? false - }; - }); - return db.collection('mm-members').insertMany(validMmMembers); + const validMmMembers = mmMembers.map(mmMember => { + return { + eventId: mmMember.eventId, + name: mmMember.name, + city: mmMember.city, + isPresent: mmMember.isPresent ?? false, + isMM: mmMember.isMM ?? false, + replacedBy: mmMember.replacedBy ?? null + }; + }); + return db.collection('mm-members').insertMany(validMmMembers); }; export const updateMmMember = ( - filter: Filter, - newMmMember: Partial, - upsert = false + filter: Filter, + newMmMember: Partial, + upsert = false ) => { - return db.collection('mm-members').updateOne(filter, { $set: newMmMember }, { upsert }); + return db.collection('mm-members').updateOne(filter, { $set: newMmMember }, { upsert }); }; export const deleteMmMember = (filter: Filter) => { - return db.collection('mm-members').deleteOne(filter); + return db.collection('mm-members').deleteOne(filter); }; export const deleteMmMembers = (filter: Filter) => { - return db.collection('mm-members').deleteMany(filter); + return db.collection('mm-members').deleteMany(filter); }; diff --git a/libs/database/src/lib/crud/rounds.ts b/libs/database/src/lib/crud/rounds.ts index 12ec19b..71e68ef 100644 --- a/libs/database/src/lib/crud/rounds.ts +++ b/libs/database/src/lib/crud/rounds.ts @@ -26,6 +26,6 @@ export const deleteRound = (filter: Filter) => { return db.collection('rounds').deleteOne(filter); }; -export const deleteRounds = () => { - return db.collection('rounds').deleteMany(); +export const deleteRounds = (filter: Filter) => { + return db.collection('rounds').deleteMany(filter); }; diff --git a/libs/database/src/lib/crud/users.ts b/libs/database/src/lib/crud/users.ts index 402cfce..663ae89 100644 --- a/libs/database/src/lib/crud/users.ts +++ b/libs/database/src/lib/crud/users.ts @@ -2,12 +2,12 @@ import { ObjectId, Filter, WithId } from 'mongodb'; import { User, SafeUser } from '@mtes/types'; import db from '../database'; -export const getEventUsersWithCredentials = () => { - return db.collection('users').find().toArray(); +export const getEventUsersWithCredentials = (eventId: ObjectId) => { + return db.collection('users').find({ eventId }).toArray(); }; -export const getEventUsers = (): Promise>> => { - return getEventUsersWithCredentials().then(users => { +export const getEventUsers = (eventId: ObjectId): Promise>> => { + return getEventUsersWithCredentials(eventId).then(users => { return users.map(user => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { password, lastPasswordSetDate, ...safeUser } = user; diff --git a/libs/database/src/lib/crud/votes.ts b/libs/database/src/lib/crud/votes.ts index c69ea52..5e972ca 100644 --- a/libs/database/src/lib/crud/votes.ts +++ b/libs/database/src/lib/crud/votes.ts @@ -69,13 +69,13 @@ export async function deleteRoundVotes(roundId: string) { } // Delete all votes -export const deleteVotes = () => { - return db.collection('votes').deleteMany({}); +export const deleteVotes = (filter: Filter) => { + return db.collection('votes').deleteMany(filter); }; // Delete all voting statuses -export const deleteVotingStatuses = () => { - return db.collection('votingStatus').deleteMany({}); +export const deleteVotingStatuses = (filter: Filter) => { + return db.collection('votingStatus').deleteMany(filter); }; // Check if round is locked diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index 85f4562..6f4eb00 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -12,4 +12,4 @@ export * from './lib/schemas/round'; export * from './lib/schemas/city'; // Added City schema export export * from './lib/positions'; export * from './lib/schemas/vote'; -export * from './lib/voting-states' +export * from './lib/voting-states'; diff --git a/libs/types/src/lib/constants.ts b/libs/types/src/lib/constants.ts index 5df051a..b87ddfd 100644 --- a/libs/types/src/lib/constants.ts +++ b/libs/types/src/lib/constants.ts @@ -88,7 +88,11 @@ export const TicketTypes = ['general', 'schedule', 'utilities', 'incident'] as c export type TicketType = (typeof TicketTypes)[number]; export const AudienceDisplayScreenTypes = [ - 'round', 'presence', 'voting', 'member', 'message' + 'round', + 'presence', + 'voting', + 'member', + 'message' ] as const; export type AudienceDisplayScreen = (typeof AudienceDisplayScreenTypes)[number]; diff --git a/libs/types/src/lib/schemas/city.ts b/libs/types/src/lib/schemas/city.ts index 81d856c..2f2e222 100644 --- a/libs/types/src/lib/schemas/city.ts +++ b/libs/types/src/lib/schemas/city.ts @@ -1,4 +1,4 @@ export interface City { - name: string; - numOfVoters: number; + name: string; + numOfVoters: number; } diff --git a/libs/types/src/lib/schemas/election-state.ts b/libs/types/src/lib/schemas/election-state.ts index 151e63b..095e2f8 100644 --- a/libs/types/src/lib/schemas/election-state.ts +++ b/libs/types/src/lib/schemas/election-state.ts @@ -1,10 +1,16 @@ -import { WithId } from 'mongodb'; +import { ObjectId, WithId } from 'mongodb'; import { AudienceDisplayScreen } from '../constants'; import { Round } from './round'; import { Member } from './member'; export interface ElectionState { + eventId: ObjectId; activeRound: WithId; - audienceDisplay: { display: AudienceDisplayScreen; round?: WithId; member?: WithId; message?: string }; + audienceDisplay: { + display: AudienceDisplayScreen; + round?: WithId; + member?: WithId; + message?: string; + }; completed: boolean; } diff --git a/libs/types/src/lib/schemas/member.ts b/libs/types/src/lib/schemas/member.ts index 39fb121..f8f556d 100644 --- a/libs/types/src/lib/schemas/member.ts +++ b/libs/types/src/lib/schemas/member.ts @@ -1,9 +1,10 @@ import { ObjectId, WithId } from 'mongodb'; import { Cities } from '../cities'; export interface Member { + eventId: ObjectId; name: string; city: Cities | 'אין אמון באת אחד'; isPresent: boolean; replacedBy?: WithId | null; isMM: boolean; -} \ No newline at end of file +} diff --git a/libs/types/src/lib/schemas/round.ts b/libs/types/src/lib/schemas/round.ts index e50daef..83801c3 100644 --- a/libs/types/src/lib/schemas/round.ts +++ b/libs/types/src/lib/schemas/round.ts @@ -1,4 +1,4 @@ -import { WithId } from 'mongodb'; +import { ObjectId, WithId } from 'mongodb'; import { Positions } from '../positions'; import { Member } from './member'; @@ -11,6 +11,7 @@ interface RoleConfig { } export interface Round { + eventId: ObjectId; name: string; roles: RoleConfig[]; allowedMembers: WithId[]; diff --git a/libs/types/src/lib/schemas/user.ts b/libs/types/src/lib/schemas/user.ts index 7223923..0a409e6 100644 --- a/libs/types/src/lib/schemas/user.ts +++ b/libs/types/src/lib/schemas/user.ts @@ -1,6 +1,8 @@ +import { ObjectId } from 'mongodb'; import { Role } from '../roles'; export interface User { + eventId?: ObjectId; username?: string; isAdmin: boolean; role?: Role; diff --git a/libs/types/src/lib/websocket.ts b/libs/types/src/lib/websocket.ts index 526638b..4e3f2ee 100644 --- a/libs/types/src/lib/websocket.ts +++ b/libs/types/src/lib/websocket.ts @@ -3,7 +3,6 @@ import { AwardNames, TicketType } from './constants'; import { Member } from './schemas/member'; import { Round } from './schemas/round'; - export interface WSServerEmittedEvents { votingMemberLoaded: (member: WithId, votingStand: number) => void; roundLoaded: (roundId: string) => void; @@ -13,9 +12,12 @@ export interface WSServerEmittedEvents { isPresent: boolean, replacedBy: WithId | null ) => void; - audienceDisplayUpdated: ( - view: { display: 'round' | 'presence' | 'voting' | 'member' | 'message'; round?: WithId; member?: WithId; message?: string } - ) => void; + audienceDisplayUpdated: (view: { + display: 'round' | 'presence' | 'voting' | 'member' | 'message'; + round?: WithId; + member?: WithId; + message?: string; + }) => void; } export interface WSClientEmittedEvents { @@ -53,7 +55,12 @@ export interface WSClientEmittedEvents { ) => void; updateAudienceDisplay: ( - view: { display: 'round' | 'presence' | 'voting' | 'member' | 'message'; round?: WithId; member?: WithId; message?: string }, + view: { + display: 'round' | 'presence' | 'voting' | 'member' | 'message'; + round?: WithId; + member?: WithId; + message?: string; + }, callback: (response: { ok: boolean; error?: string }) => void ) => void; } diff --git a/nx.json b/nx.json index 084b9d5..502a8c2 100644 --- a/nx.json +++ b/nx.json @@ -2,13 +2,8 @@ "$schema": "./node_modules/nx/schemas/nx-schema.json", "defaultBase": "master", "namedInputs": { - "default": [ - "{projectRoot}/**/*", - "sharedGlobals" - ], - "production": [ - "default" - ], + "default": ["{projectRoot}/**/*", "sharedGlobals"], + "production": ["default"], "sharedGlobals": [] }, "plugins": [ @@ -53,23 +48,13 @@ "targetDefaults": { "@nx/js:tsc": { "cache": true, - "dependsOn": [ - "^build" - ], - "inputs": [ - "production", - "^production" - ] + "dependsOn": ["^build"], + "inputs": ["production", "^production"] }, "@nx/esbuild:esbuild": { "cache": true, - "dependsOn": [ - "^build" - ], - "inputs": [ - "production", - "^production" - ] + "dependsOn": ["^build"], + "inputs": ["production", "^production"] } } }