diff --git a/package-lock.json b/package-lock.json index f3fad4d..710d15b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/sdk-trace-node": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", + "airtable": "^0.12.2", "archiver": "^7.0.1", "axios": "^1.9.0", "body-parser": "^2.2.0", @@ -5248,6 +5249,11 @@ "node": ">=6.5" } }, + "node_modules/abortcontroller-polyfill": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.8.tgz", + "integrity": "sha512-9f1iZ2uWh92VcrU9Y8x+LdM4DLj75VE0MJB8zuF1iUnroEptStw+DQ8EQPMUdfe5k+PkB1uUfDQfWbhstH8LrQ==" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -5319,6 +5325,26 @@ "node": ">= 14" } }, + "node_modules/airtable": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/airtable/-/airtable-0.12.2.tgz", + "integrity": "sha512-HS3VytUBTKj8A0vPl7DDr5p/w3IOGv6RXL0fv7eczOWAtj9Xe8ri4TAiZRXoOyo+Z/COADCj+oARFenbxhmkIg==", + "dependencies": { + "@types/node": ">=8.0.0 <15", + "abort-controller": "^3.0.0", + "abortcontroller-polyfill": "^1.4.0", + "lodash": "^4.17.21", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/airtable/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -10373,10 +10399,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -10396,28 +10419,19 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/node-fetch/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true + "license": "BSD-2-Clause" }, "node_modules/node-fetch/node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" diff --git a/package.json b/package.json index 25b8d44..d681d4e 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/sdk-trace-node": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", + "airtable": "^0.12.2", "archiver": "^7.0.1", "axios": "^1.9.0", "body-parser": "^2.2.0", diff --git a/src/controller/NfcController.ts b/src/controller/NfcController.ts index f5eae10..7e42366 100644 --- a/src/controller/NfcController.ts +++ b/src/controller/NfcController.ts @@ -1,5 +1,8 @@ +import axios from "axios"; import { mongoose } from "../services/mongoose_service"; const ObjectId = mongoose.Types.ObjectId; +import Airtable from "airtable"; +import { getModels } from "./util/resources"; const NfcSchema = new mongoose.Schema({ nfcId: { type: String, required: true }, @@ -7,14 +10,9 @@ const NfcSchema = new mongoose.Schema({ createdAt: { type: Date, default: Date.now } }); -const UserSchema = new mongoose.Schema({ - checkIns: { type: Object, default: {} } -}) - const NfcModel = mongoose.model('nfc-user-assignments', NfcSchema); -delete mongoose.models.User; -const UserModel = mongoose.model('User', UserSchema); +const NfcUserModel = getModels().user.mongoose; export const assignNFCToUser = async (nfcId: string, userId: string) => { if (!nfcId || !userId) { @@ -95,7 +93,7 @@ export const getUserFromNfcId = async (nfcId: string) => { } try { - const user = await UserModel.findById(userId); + const user = await NfcUserModel.findById(userId); return user; } catch (error: any) { console.log(error); @@ -104,28 +102,128 @@ export const getUserFromNfcId = async (nfcId: string) => { } -export const updateCheckInField = async (nfcId: string, field: string, value: boolean) => { +export const checkIn = async (nfcId: string, field: string) => { const userId = await getUserIdFromNfcId(nfcId); - const existingUser = await UserModel.findById(userId); + const existingUser = await NfcUserModel.findById(userId); if (!existingUser) { throw new Error(`User with ID ${userId} does not exist.`); } + if (!existingUser.checkIns) { + throw new Error(`User with ID ${userId} does not have any events.`); + } + + const newCheckIn = new Date().toISOString(); + try { - const response = await UserModel.updateOne( + const response = await NfcUserModel.updateOne( { _id: new ObjectId(userId), checkIns: { $exists: true } }, { $set: { - checkIns: { - ...existingUser.checkIns, - [field]: value - } - + checkIns: existingUser.checkIns.map((checkIn) => { + if (checkIn.event.name === field) { + return { + ...checkIn, + checkIns: [...checkIn.checkIns, newCheckIn] + } + } + return checkIn; + }) } }, ); - return response; + + + return newCheckIn; } catch (error: any) { console.log(error); throw new Error(`Error updating check-ins: ${error.message}`); } } + +export const removeLastCheckIn = async (nfcId: string, field: string) => { + const userId = await getUserIdFromNfcId(nfcId); + + const existingUser = await NfcUserModel.findById(userId); + if (!existingUser) { + throw new Error(`User with ID ${userId} does not exist.`); + } + + if (!existingUser.checkIns) { + throw new Error(`User with ID ${userId} does not have any events.`); + } + + const newCheckIns = existingUser.checkIns.map((checkIn: any) => { + if (checkIn.event.name === field) { + return { + ...checkIn, + checkIns: checkIn.checkIns.slice(0, -1) + } + } + return checkIn; + }); + try { + const response = await NfcUserModel.updateOne( + { _id: new ObjectId(userId), checkIns: { $exists: true } }, + { $set: { checkIns: newCheckIns } } + ); + } catch (error: any) { + console.log(error); + throw new Error(`Error removing last check-in: ${error.message}`); + } +} + +export const populateEvents = async (userId: string) => { + const airtableToken = process.env.AIRTABLE_TOKEN; + + let events: any[] = []; + try { + + console.log('airtableToken', airtableToken); + + Airtable.configure({ + endpointUrl: 'https://api.airtable.com', + apiKey: airtableToken + }); + + const base = Airtable.base("app8WOptWZhwtlUam"); + + const records = await base('Events').select({ + fields: ['Name', 'Start', 'End'] + }).all(); + events = [...records]; + } catch (error: any) { + console.log(error); + throw new Error(`Error populating events: ${error.message}`); + } + + console.log(events); + + const eventsArray = events.map((event: any) => { + return { + event: { + name: event.fields.Name, + start: event.fields.Start, + end: event.fields.End + }, + checkIns: [] // when hackers check in append a some sort of date/time to the checkIns array + } + }); + + try { + + const user = await NfcUserModel.findByIdAndUpdate( + userId, + { + $set: { + checkIns: eventsArray + } + }, + { new: true } + ); + + return user; + } catch (error: any) { + console.log(error); + throw new Error(`Error populating events: ${error.message}`); + } +} diff --git a/src/models/user/fields.ts b/src/models/user/fields.ts index 20d82f4..05bf1b1 100644 --- a/src/models/user/fields.ts +++ b/src/models/user/fields.ts @@ -916,6 +916,13 @@ export const fields = { readCheck: true, }, + checkIns: { + type: Array, + required: false, + default: [], + readCheck: true, + }, + lastLogout: { type: Number, required: true, @@ -1151,6 +1158,14 @@ export interface IUser extends BasicUser { computedApplicationDeadline: number; computedRSVPDeadline: number; mailingListSubcriberID?: number; + checkIns?: { + event: { + name: string; + start: string; // "2025-07-18T21:00:00.000Z" + end: string; + }; + checkIns: string[]; // ISO string + }[]; } export interface IMailMerge { diff --git a/src/routes/nfc.ts b/src/routes/nfc.ts index b2cf6ea..578a782 100644 --- a/src/routes/nfc.ts +++ b/src/routes/nfc.ts @@ -2,7 +2,16 @@ import express, {Request, Response} from "express"; import { isConnected } from "../services/mongoose_service"; -import { assignNFCToUser, getUserIdFromNfcId, getUserFromNfcId, deleteAssignmentByNfc, deleteAssignmentByUser, updateCheckInField } from "../controller/NfcController"; +import { + assignNFCToUser, + getUserIdFromNfcId, + getUserFromNfcId, + deleteAssignmentByNfc, + deleteAssignmentByUser, + checkIn, + populateEvents, + removeLastCheckIn +} from "../controller/NfcController"; import { isVolunteer } from "../models/validator"; @@ -57,30 +66,37 @@ nfcRouter.get('/getUser/:nfcId', async (req: Request, res: Response) => { } }); -nfcRouter.post('/updateCheckInsFromNFC', async (req: Request, res: Response) => { - const { nfcId, checkInEvent, value } = req.body; - - const events = [ - 'hackerCheckIn', - 'lunchOne', - 'dinnerOne', - 'eventOne', - 'snackOne', - 'lunchTwo', - 'dinnerTwo' - ] - - if (checkInEvent === undefined || !events.includes(checkInEvent)) { - return res.status(400).json({ error: 'Invalid check-in event' }); +nfcRouter.post('/checkInFromNFC', async (req: Request, res: Response) => { + const { nfcId, checkInEvent } = req.body; + + try { + const response = await checkIn(nfcId, checkInEvent); + return res.status(200).json({ response }); + } catch (error) { + return res.status(500).json({ error: 'Failed to update check-ins' }); } +}); + +nfcRouter.post('/removeLastCheckIn', async (req: Request, res: Response) => { + const { nfcId, checkInEvent } = req.body; try { - const response = await updateCheckInField(nfcId, checkInEvent, value); + const response = await removeLastCheckIn(nfcId, checkInEvent); return res.status(200).json({ response }); } catch (error) { return res.status(500).json({ error: 'Failed to update check-ins' }); } +}); + +nfcRouter.post('/populateEvents', async (req: Request, res: Response) => { + const { userId } = req.body; + try { + const response = await populateEvents(userId); + return res.status(200).json({ response }); + } catch (error) { + return res.status(500).json({ error: 'Failed to populate events' }); + } }); export default nfcRouter;