/* This code is part of Blink licensed under GPLv3 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ require('dotenv').config(); const bcrypt = require('bcrypt'); const crypto = require('crypto'); const knex = require('knex')({ client: 'pg', connection: { host: process.env.POSTGRES_SERVER, user: process.env.POSTGRES_USERNAME, password: process.env.POSTGRES_PASSWORD, port: process.env.POSTGRES_PORT, database: 'Blink' } }); const jwt = require('jsonwebtoken'); // ======== BEGIN API ENDPOINTS ======== /** * POST Request * * Register a Person * * @returns The activationLink identifier if * the registration is successful */ async function registerPerson(req, res){ if (process.env.ALLOW_USER_REGISTRATION === 'false'){ return res.status(403).json({error : "Users cannot register on this server"}); } // Ensure that the required fields are present before proceeding if (!req.body.display_name || !req.body.email || !req.body.password) { return res.status(400).json({ error : "Some or all required fields are missing"}); } if(!validateEmail(req.body.email)){ return res.status(400).json({ error : "The email is not in a valid format"}); } // Generate activation link token const activationLink = crypto.randomBytes(16).toString('hex'); // Hash provided password const hashPasswordPromise = bcrypt.hash(req.body.password, 10); try{ // We need to insert either both in the "Person" table // and in the "ActivationLink" one, or in neither await knex.transaction(async (tr) => { const personIdResult = await tr('Person') .insert({ email: req.body.email, password: await hashPasswordPromise, display_name: req.body.display_name, date_of_birth: req.body.date_of_birth, available: req.body.available, enabled: true, place_of_living: req.body.place_of_living }) .returning("id"); await tr('ActivationLink') .insert({ person_id: personIdResult[0].id, identifier: activationLink }); }); return res.status(200).json({ activationLink: activationLink }); } catch (error){ console.error('Error registering person:', error); res.status(500).json({error : "Internal server error"}); } } /** * POST Request * * Creates a token if the specified email * and password are valid * * @returns The token */ async function login(req, res){ // Ensure that the required fields are present before proceeding if (!req.body.email || !req.body.password) { return res.status(400).json({error : "Invalid request"}); } const person = await checkUserCredentials(req.body.email, req.body.password); if (person){ const token = generateToken(person.id); res.status(200).json({token: token }); } else{ res.status(401).json({error : "Unauthorized"}); } } /** * Obtain a Person's details if the * specified person is myself or an * enabled Person * * @returns The Person */ async function getPerson(req, res){ try { const user = await knex('Person') .select('*') .where({ id: req.params.id, enabled: true }) .first(); if(user){ // I am retrieving myself or an enabled user if(user.id == req.jwt.person_id || user.enabled){ delete user['password']; // remove password field for security reasons return res.status(200).send(user); } } return res.status(404).json({error: "Not found"}); } catch (error) { console.log("Error while getting person: " + error); return res.status(500).json({error : "Internal server error"}); } } /** * PUT request * * Updates a Person's details. If some details are * not present, they shall be ignored. * To update the password, both the old_password * and new_password field must be specified. * */ async function updatePerson(req, res){ if (req.jwt.person_id != req.params.id){ return res.status(403).json({ error : "Forbidden"}); } const updatePerson = {}; if(req.body.display_name){ updatePerson.display_name = req.body.display_name; } if(req.body.date_of_birth){ if(isPostgresDateFormatValid(req.body.date_of_birth)){ updatePerson.date_of_birth = req.body.date_of_birth; } else{ return res.status(400).json({ error : "Date of birth format not valid. Please specify a YYYY-MM-DD date"}); } } if(req.body.available){ updatePerson.available = req.body.available; } if(req.body.place_of_living){ updatePerson.place_of_living = req.body.place_of_living; } // If we are tying to change password, the old password must be provided if(req.body.old_password && req.body.new_password){ const user = await knex('Person') .select('password') .where({ id: req.jwt.person_id }) .first(); const passwordMatches = await bcrypt.compare(req.body.old_password, user.password); if(passwordMatches){ updatePerson.password = await bcrypt.hash(req.body.new_password, 10); } else{ return res.status(401).json({ error : "Password verification failed"}); } } if (Object.keys(updatePerson).length === 0) { return res.status(400).json({ error : "Bad request. No data to update"}); } try { await knex('Person') .where('id', req.params.id) .update(updatePerson); return res.status(200).json({ success : "true"}); } catch (error) { console.log("Error while updating a Person: " + error); return res.status(500).json({ error : "Internal server error"}); } } /** * GET Request * * Deletes a User. An user can only delete * themselves. * */ async function deletePerson(req, res) { try { await knex('Person') .where({id : req.jwt.person_id}) .del(); return res.status(200).json({success: true}); } catch (error) { console.log("Error deleting a Person: " + error); return res.status(500).json({error : "Internal server error"}); } } // POST async function createOrganization(req, res){ // Ensure that the required fields are present before proceeding if (!req.body.name) { return res.status(400).json({ error : "Invalid request"}); } try{ await knex.transaction(async (trx) => { // We have to insert either both in Organization and in OrganizationAdministrator // or in neither const organizationResult = await trx('Organization') .insert({ name: req.body.name, location: req.body.location, description: req.body.description, is_hiring: req.body.is_hiring, }) .returning('*'); // Inserting in the "OrganizationAdministrator" table await trx('OrganizationAdministrator') .insert({ id_person: req.jwt.person_id, id_organization: organizationResult[0].id, }); await trx.commit(); return res.status(200).json({ Organization: organizationResult[0] }); }); } catch (error){ console.error('Error creating Organization:', error); res.status(500).json({error : "Internal server error"}); } } // PUT async function updateOrganization(req, res){ if(!req.body.name || req.body.name.trim().length === 0){ return res.status(400).json({ error : "Invalid request"}); } try { await knex.transaction(async (trx) => { // Check if the current user is a organization's administrator const isOrganizationAdmin = await trx('OrganizationAdministrator') .where('id_person', req.jwt.person_id) .where('id_organization', req.params.id) .select('*') .first(); if(!isOrganizationAdmin){ return res.status(403).json({error : "Forbidden"}); } await knex('Organization') .where('id', req.params.id) .update({ name: req.body.name, location: req.body.location, description: req.body.description, is_hiring: req.body.is_hiring }); return res.status(200).json({ success : "true"}); }); } catch (error) { console.log(error); return res.status(500).json({error : "Internal server error"}); } } // DELETE async function deleteOrganization(req, res){ const organizationIdToDelete = req.params.id; try { // Here we do not actually need a transaction. Two different queries, // one who checks if the user is admin and one to delete the organization would've // been sufficient and non-exploitable, but still it'd have been a // TOC/TOU weakness (https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use). // Whether a good practice or not is matter of debate. // There are other points in the code using the same technique to address the same // problem knex.transaction(async (trx) => { // Check if the current user is a organization's administrator const isOrganizationAdmin = await trx('OrganizationAdministrator') .where('id_person', req.jwt.person_id) .where('id_organization', req.body.organization_id) .select('*') .first(); if(!isOrganizationAdmin){ return res.status(403).json({error : "Forbidden"}); } await knex('Organization') .where({ id: organizationIdToDelete }) .del(); return res.status(200).json({success: true}); }); } catch (error) { console.error(error); return res.status(500).json({error : "Internal server error"}); } } // POST async function createOrganizationPost(req, res){ // Ensure that the required fields are present before proceeding if (!req.body.organization_id || !req.body.content) { return res.status(400).json({ error : "Invalid request"}); } try { knex.transaction(async (trx) => { // Check if the current user is a organization's administrator const isOrganizationAdmin = await trx('OrganizationAdministrator') .where('id_person', req.jwt.person_id) .where('id_organization', req.body.organization_id) .select('*') .first(); if(!isOrganizationAdmin){ return res.status(403).json({error : "Forbidden"}); } const organizationPost = await knex('OrganizationPost') .insert({ organization_id: req.body.organization_id, content: req.body.content, original_author: req.jwt.person_id }) .returning('*'); return res.status(200).json(organizationPost[0]); }); } catch (error) { console.log(error); return res.status(500).json({error : "Internal server error"}); } } // GET async function getOrganization(req, res){ const organizationId = req.params.id; try { const organization = await knex('Organization') .where('id', organizationId) .select('*') .first(); if(organization) { return res.status(200).json(organization); } else{ return res.status(404).json({error : "Not found"}); } } catch (error) { console.error("Error retrieving an organization: " + error); return res.status(500).json({error : "Internal server error"}); } } // DELETE async function deleteOrganizationPost(req, res){ const organizationPostIdToDelete = req.params.id; try{ knex.transaction(async (trx) => { // Check if user is allowed to delete the post (they must have created it) const isOrganizationAdmin = await trx('OrganizationPost') .join('OrganizationAdministrator', 'OrganizationPost.organization_id', 'OrganizationAdministrator.id_organization') .where('OrganizationPost.id', organizationPostIdToDelete) .where('OrganizationAdministrator.id_person', req.jwt.person_id) .select('*') .first(); if (isOrganizationAdmin) { await trx('OrganizationPost') .where('id', organizationPostIdToDelete) .del(); await trx.commit(); return res.status(200).json({success: true}); } else { return res.status(401).json({error : "Forbidden"}); } }); } catch (error) { console.log(error); res.status(500).json({error : "Internal server error"}); } } // POST async function addOrganizationAdmin(req, res){ // Ensure that the required fields are present before proceeding if (!req.body.organization_id || !req.body.person_id) { return res.status(400).json({ error : "Invalid request"}); } try { knex.transaction(async (trx) => { // Check if the current user is a organization's administrator const result = await trx('OrganizationAdministrator') .where('id_person', req.jwt.person_id) .where('id_organization', req.body.organization_id) .select('*') .first(); if(!result){ return res.status(401).json({error : "Forbidden"}); } // We suppose that the database has Foreign Key constraints // otherwise we should've checked whether person_id exists. await knex('OrganizationAdministrator') .insert({ id_person: req.body.person_id, id_organization: req.body.organization_id }); return res.status(200).json({success : true}); }); } catch (error) { console.error('Error while adding organization admin: ' + error); res.status(500).json({error : "Internal server error"}); } } // DELETE async function removeOrganizationAdmin(req, res){ // Ensure that the required fields are present before proceeding if (!req.body.organization_id || !req.body.person_id) { return res.status(400).json({ error : "Invalid request"}); } // I can remove only myself from the list of administrators // TODO: What's the point for having 'body.person_id' then? if(req.body.person_id != req.jwt.person_id){ return res.status(403).json({ error : "Forbidden"}); } try{ knex.transaction(async (trx) => { await trx('OrganizationAdministrator') .where('id_person', req.jwt.person_id) .where('id_organization', req.body.organization_id) .del(); // Delete Organization if there are no admins left. // TODO: If the user instead deletes their entire profile, the organization will not be deleted. Fix. // TODO: Check what level of transaction we are using to avoid inconsistencies. Update: it is READ COMMITTED see https://www.geeksforgeeks.org/transaction-isolation-levels-dbms/ const count = await trx('OrganizationAdministrator') .count('id as count') .where('id', req.body.organization_id); if(count[0].count == 1){ await trx('Organization') .where('id', req.body.organization_id) .del(); } return res.status(200).json({success : true}); }); } catch (error){ console.error(error); return res.status(500).json({ error: "Internal server error"}); } } // ======== END API ENDPOINTS ======== async function checkUserCredentials(email, password){ try { const user = await knex('Person') .where('email', email) .where('enabled', true) .select('*') .first(); if(user){ const passwordMatches = await bcrypt.compare(password, user.password); if (passwordMatches) { return user; } } return null; } catch (error) { console.log(error); return null; } } function generateToken(person_id) { // The payload the JWT will carry within itself const payload = { person_id: person_id }; const token = jwt.sign(payload, process.env.JWT_SECRET_KEY, { expiresIn: '8h' }); return token; } // Middlware function verifyToken(req, res, next) { const token = req.headers.authorization; if (!token) { return res.status(401).send({error : 'No token provided'}); } jwt.verify(token, process.env.JWT_SECRET_KEY, (err, decoded) => { if (err) { return res.status(401).send({error : 'Failed to authenticate token'}); } // If the token is valid, store the decoded data in the request object req.jwt = decoded; next(); }); } function validateEmail(email) { const regex = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/; return regex.test(email); } function isPostgresDateFormatValid(dateString) { const regex = /^\d{4}-\d{2}-\d{2}$/; return regex.test(dateString); } // Exporting a function // means making a JavaScript function defined in one // module available for use in another module. module.exports = { registerPerson, login, getPerson, updatePerson, deletePerson, verifyToken, createOrganization, getOrganization, updateOrganization, deleteOrganization, createOrganizationPost, deleteOrganizationPost, addOrganizationAdmin, removeOrganizationAdmin };