From 151baf71253ddb1152fff5a4840704076b904377 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 24 Jun 2026 15:52:42 +0000 Subject: [PATCH] Autosave: 20260624-155247 --- backend/src/ai/LocalAIApi.js | 16 +- backend/src/auth/auth.js | 2 +- backend/src/db/api/assessments.js | 53 +-- backend/src/db/api/attendance.js | 49 +-- backend/src/db/api/book_loans.js | 51 +-- backend/src/db/api/books.js | 47 +-- backend/src/db/api/classes.js | 49 +-- backend/src/db/api/courses.js | 47 +-- backend/src/db/api/employees.js | 47 +-- backend/src/db/api/enrollments.js | 51 +-- backend/src/db/api/file.js | 10 +- backend/src/db/api/grades.js | 47 +-- backend/src/db/api/guardians.js | 47 +-- backend/src/db/api/invoices.js | 49 +-- backend/src/db/api/payments.js | 68 ++-- backend/src/db/api/permissions.js | 22 +- backend/src/db/api/products.js | 47 +-- backend/src/db/api/roles.js | 22 +- backend/src/db/api/schoolScope.js | 103 +++++ backend/src/db/api/schools.js | 34 +- backend/src/db/api/student_guardians.js | 51 +-- backend/src/db/api/students.js | 66 ++- backend/src/db/api/subjects.js | 47 +-- backend/src/db/api/teachers.js | 47 +-- backend/src/db/api/users.js | 134 ++++-- backend/src/db/migrations/1782175325418.js | 2 +- backend/src/db/models/assessments.js | 5 - backend/src/db/models/attendance.js | 5 - backend/src/db/models/book_loans.js | 5 - backend/src/db/models/books.js | 5 - backend/src/db/models/classes.js | 5 - backend/src/db/models/courses.js | 5 - backend/src/db/models/employees.js | 5 - backend/src/db/models/enrollments.js | 5 - backend/src/db/models/grades.js | 5 - backend/src/db/models/guardians.js | 5 - backend/src/db/models/invoices.js | 5 - backend/src/db/models/payments.js | 5 - backend/src/db/models/permissions.js | 5 - backend/src/db/models/products.js | 5 - backend/src/db/models/roles.js | 5 - backend/src/db/models/student_guardians.js | 5 - backend/src/db/models/students.js | 5 - backend/src/db/models/subjects.js | 5 - backend/src/db/models/teachers.js | 5 - backend/src/db/models/users.js | 17 +- .../db/seeders/20200430130759-admin-user.js | 2 +- .../db/seeders/20200430130760-user-roles.js | 2 +- .../db/seeders/20231127130745-sample-data.js | 4 +- backend/src/helpers.js | 4 +- backend/src/index.js | 1 - backend/src/middlewares/check-permissions.js | 16 +- backend/src/routes/assessments.js | 9 +- backend/src/routes/attendance.js | 9 +- backend/src/routes/book_loans.js | 9 +- backend/src/routes/books.js | 9 +- backend/src/routes/classes.js | 9 +- backend/src/routes/courses.js | 9 +- backend/src/routes/employees.js | 9 +- backend/src/routes/enrollments.js | 9 +- backend/src/routes/file.js | 20 +- backend/src/routes/grades.js | 9 +- backend/src/routes/guardians.js | 9 +- backend/src/routes/invoices.js | 9 +- backend/src/routes/openai.js | 174 ++++++-- backend/src/routes/payments.js | 9 +- backend/src/routes/permissions.js | 1 + backend/src/routes/products.js | 9 +- backend/src/routes/roles.js | 3 +- backend/src/routes/schools.js | 9 +- backend/src/routes/search.js | 32 +- backend/src/routes/sql.js | 26 +- backend/src/routes/student_guardians.js | 9 +- backend/src/routes/students.js | 9 +- backend/src/routes/subjects.js | 9 +- backend/src/routes/teachers.js | 9 +- backend/src/routes/users.js | 9 +- backend/src/services/assessments.js | 31 +- backend/src/services/attendance.js | 31 +- backend/src/services/auth.js | 16 +- backend/src/services/book_loans.js | 31 +- backend/src/services/books.js | 31 +- backend/src/services/classes.js | 31 +- backend/src/services/courses.js | 31 +- backend/src/services/employees.js | 31 +- backend/src/services/enrollments.js | 31 +- backend/src/services/file.js | 302 ++++++++++++-- backend/src/services/grades.js | 31 +- backend/src/services/guardians.js | 31 +- backend/src/services/importFileParser.js | 380 ++++++++++++++++++ backend/src/services/invoices.js | 31 +- backend/src/services/openai.js | 15 +- backend/src/services/payments.js | 31 +- backend/src/services/permissions.js | 29 +- backend/src/services/products.js | 31 +- backend/src/services/roles.js | 222 +++++----- backend/src/services/schools.js | 23 +- backend/src/services/search.js | 199 +++++---- backend/src/services/sqlSafety.js | 83 ++++ backend/src/services/student_guardians.js | 31 +- backend/src/services/students.js | 31 +- backend/src/services/subjects.js | 31 +- backend/src/services/teachers.js | 31 +- backend/src/services/users.js | 28 +- frontend/public/locales/en/common.json | 2 +- frontend/public/locales/pt/common.json | 2 +- .../src/components/DragDropFilePicker.tsx | 32 +- frontend/src/components/FormFilePicker.tsx | 28 +- .../src/components/Uploaders/UploadService.js | 18 +- frontend/src/config.ts | 12 + .../pages/assessments/assessments-list.tsx | 8 +- .../pages/assessments/assessments-table.tsx | 8 +- .../src/pages/attendance/attendance-list.tsx | 8 +- .../src/pages/attendance/attendance-table.tsx | 8 +- .../src/pages/book_loans/book_loans-list.tsx | 8 +- .../src/pages/book_loans/book_loans-table.tsx | 8 +- frontend/src/pages/books/books-list.tsx | 8 +- frontend/src/pages/books/books-table.tsx | 8 +- frontend/src/pages/classes/classes-list.tsx | 8 +- frontend/src/pages/classes/classes-table.tsx | 8 +- frontend/src/pages/courses/courses-list.tsx | 8 +- frontend/src/pages/courses/courses-table.tsx | 8 +- .../src/pages/employees/employees-list.tsx | 8 +- .../src/pages/employees/employees-table.tsx | 8 +- .../pages/enrollments/enrollments-list.tsx | 8 +- .../pages/enrollments/enrollments-table.tsx | 8 +- frontend/src/pages/grades/grades-list.tsx | 8 +- frontend/src/pages/grades/grades-table.tsx | 8 +- .../src/pages/guardians/guardians-list.tsx | 8 +- .../src/pages/guardians/guardians-table.tsx | 8 +- frontend/src/pages/invoices/invoices-list.tsx | 8 +- .../src/pages/invoices/invoices-table.tsx | 8 +- frontend/src/pages/payments/payments-list.tsx | 8 +- .../src/pages/payments/payments-table.tsx | 8 +- .../pages/permissions/permissions-list.tsx | 8 +- .../pages/permissions/permissions-table.tsx | 8 +- frontend/src/pages/products/products-list.tsx | 8 +- .../src/pages/products/products-table.tsx | 8 +- frontend/src/pages/profile.tsx | 51 +-- frontend/src/pages/roles/roles-list.tsx | 8 +- frontend/src/pages/roles/roles-table.tsx | 8 +- frontend/src/pages/schools/schools-list.tsx | 8 +- frontend/src/pages/schools/schools-table.tsx | 8 +- .../student_guardians-list.tsx | 8 +- .../student_guardians-table.tsx | 8 +- frontend/src/pages/students/students-list.tsx | 8 +- .../src/pages/students/students-table.tsx | 8 +- frontend/src/pages/subjects/subjects-list.tsx | 8 +- .../src/pages/subjects/subjects-table.tsx | 8 +- frontend/src/pages/teachers/teachers-list.tsx | 8 +- .../src/pages/teachers/teachers-table.tsx | 8 +- frontend/src/pages/users/users-list.tsx | 8 +- frontend/src/pages/users/users-table.tsx | 8 +- 153 files changed, 2219 insertions(+), 1862 deletions(-) create mode 100644 backend/src/db/api/schoolScope.js create mode 100644 backend/src/services/importFileParser.js create mode 100644 backend/src/services/sqlSafety.js diff --git a/backend/src/ai/LocalAIApi.js b/backend/src/ai/LocalAIApi.js index fd571ae..8e49ead 100644 --- a/backend/src/ai/LocalAIApi.js +++ b/backend/src/ai/LocalAIApi.js @@ -154,7 +154,7 @@ async function awaitResponse(aiRequestId, options = {}) { const interval = Math.max(Number(options.interval ?? 5), 1); const deadline = Date.now() + Math.max(timeout, interval) * 1000; - while (true) { + while (Date.now() < deadline) { const statusResp = await fetchStatus(aiRequestId, { headers: options.headers, timeout: options.timeout_per_call, @@ -184,16 +184,14 @@ async function awaitResponse(aiRequestId, options = {}) { return statusResp; } - if (Date.now() >= deadline) { - return { - success: false, - error: "timeout", - message: "Timed out waiting for AI response.", - }; - } - await sleep(interval * 1000); } + + return { + success: false, + error: "timeout", + message: "Timed out waiting for AI response.", + }; } function extractText(response) { diff --git a/backend/src/auth/auth.js b/backend/src/auth/auth.js index 251c149..a544932 100644 --- a/backend/src/auth/auth.js +++ b/backend/src/auth/auth.js @@ -56,7 +56,7 @@ passport.use(new MicrosoftStrategy({ )); function socialStrategy(email, profile, provider, done) { - db.users.findOrCreate({where: {email, provider}}).then(([user, created]) => { + db.users.findOrCreate({where: {email, provider}}).then(([user]) => { const body = { id: user.id, email: user.email, diff --git a/backend/src/db/api/assessments.js b/backend/src/db/api/assessments.js index 13cf956..e7c7be2 100644 --- a/backend/src/db/api/assessments.js +++ b/backend/src/db/api/assessments.js @@ -1,8 +1,7 @@ const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); +const { applySchoolScope, applySchoolScopeById, assertRecordInCurrentSchool, assertRelatedRecordInCurrentSchool, resolveSchoolIdForMutation } = require('./schoolScope'); @@ -49,18 +48,21 @@ module.exports = class AssessmentsDBApi { ); - await assessments.setSchool( data.school || null, { + await assessments.setSchool(resolveSchoolIdForMutation(data.school, currentUser), { transaction, }); + await assertRelatedRecordInCurrentSchool('students', data.student, currentUser, 'schoolId', { transaction }); await assessments.setStudent( data.student || null, { transaction, }); + await assertRelatedRecordInCurrentSchool('subjects', data.subject, currentUser, 'schoolId', { transaction }); await assessments.setSubject( data.subject || null, { transaction, }); + await assertRelatedRecordInCurrentSchool('teachers', data.teacher, currentUser, 'schoolId', { transaction }); await assessments.setTeacher( data.teacher || null, { transaction, }); @@ -105,6 +107,7 @@ module.exports = class AssessmentsDBApi { importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, + schoolId: resolveSchoolIdForMutation(item.school, currentUser), createdAt: new Date(Date.now() + index * 1000), })); @@ -120,9 +123,8 @@ module.exports = class AssessmentsDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; - const assessments = await db.assessments.findByPk(id, {}, {transaction}); + assertRecordInCurrentSchool(assessments, currentUser, 'schoolId'); @@ -150,13 +152,14 @@ module.exports = class AssessmentsDBApi { if (data.school !== undefined) { await assessments.setSchool( - data.school, + resolveSchoolIdForMutation(data.school, currentUser), { transaction } ); } if (data.student !== undefined) { + await assertRelatedRecordInCurrentSchool('students', data.student, currentUser, 'schoolId', { transaction }); await assessments.setStudent( data.student, @@ -166,6 +169,7 @@ module.exports = class AssessmentsDBApi { } if (data.subject !== undefined) { + await assertRelatedRecordInCurrentSchool('subjects', data.subject, currentUser, 'schoolId', { transaction }); await assessments.setSubject( data.subject, @@ -175,6 +179,7 @@ module.exports = class AssessmentsDBApi { } if (data.teacher !== undefined) { + await assertRelatedRecordInCurrentSchool('teachers', data.teacher, currentUser, 'schoolId', { transaction }); await assessments.setTeacher( data.teacher, @@ -205,6 +210,8 @@ module.exports = class AssessmentsDBApi { transaction, }); + assessments.forEach((record) => assertRecordInCurrentSchool(record, currentUser, 'schoolId')); + await db.sequelize.transaction(async (transaction) => { for (const record of assessments) { await record.update( @@ -226,6 +233,7 @@ module.exports = class AssessmentsDBApi { const transaction = (options && options.transaction) || undefined; const assessments = await db.assessments.findByPk(id, options); + assertRecordInCurrentSchool(assessments, currentUser, 'schoolId'); await assessments.update({ deletedBy: currentUser.id @@ -242,6 +250,9 @@ module.exports = class AssessmentsDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || null; + + applySchoolScope(where, currentUser?.app_role?.globalAccess, currentUser, 'schoolId'); const assessments = await db.assessments.findOne( { where }, @@ -309,26 +320,11 @@ module.exports = class AssessmentsDBApi { let offset = 0; let where = {}; const currentPage = +filter.page; - - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; - - - - if (userSchools) { - if (options?.currentUser?.schoolsId) { - where.schoolsId = options.currentUser.schoolsId; - } - } offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ { @@ -530,11 +526,7 @@ module.exports = class AssessmentsDBApi { } - - if (globalAccess) { - delete where.schoolsId; - } - + applySchoolScope(where, globalAccess, user, 'schoolId'); const queryOptions = { where, @@ -565,14 +557,9 @@ module.exports = class AssessmentsDBApi { } } - static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { + static async findAllAutocomplete(query, limit, offset, globalAccess, schoolId,) { let where = {}; - - if (!globalAccess && organizationId) { - where.organizationId = organizationId; - } - if (query) { where = { @@ -587,6 +574,8 @@ module.exports = class AssessmentsDBApi { }; } + applySchoolScopeById(where, globalAccess, schoolId, 'schoolId'); + const records = await db.assessments.findAll({ attributes: [ 'id', 'tipo' ], where, diff --git a/backend/src/db/api/attendance.js b/backend/src/db/api/attendance.js index 649b500..8a72736 100644 --- a/backend/src/db/api/attendance.js +++ b/backend/src/db/api/attendance.js @@ -1,8 +1,7 @@ const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); +const { applySchoolScope, applySchoolScopeById, assertRecordInCurrentSchool, assertRelatedRecordInCurrentSchool, resolveSchoolIdForMutation } = require('./schoolScope'); @@ -45,10 +44,11 @@ module.exports = class AttendanceDBApi { ); - await attendance.setSchool( data.school || null, { + await attendance.setSchool(resolveSchoolIdForMutation(data.school, currentUser), { transaction, }); + await assertRelatedRecordInCurrentSchool('students', data.student, currentUser, 'schoolId', { transaction }); await attendance.setStudent( data.student || null, { transaction, }); @@ -89,6 +89,7 @@ module.exports = class AttendanceDBApi { importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, + schoolId: resolveSchoolIdForMutation(item.school, currentUser), createdAt: new Date(Date.now() + index * 1000), })); @@ -104,9 +105,8 @@ module.exports = class AttendanceDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; - const attendance = await db.attendance.findByPk(id, {}, {transaction}); + assertRecordInCurrentSchool(attendance, currentUser, 'schoolId'); @@ -131,13 +131,14 @@ module.exports = class AttendanceDBApi { if (data.school !== undefined) { await attendance.setSchool( - data.school, + resolveSchoolIdForMutation(data.school, currentUser), { transaction } ); } if (data.student !== undefined) { + await assertRelatedRecordInCurrentSchool('students', data.student, currentUser, 'schoolId', { transaction }); await attendance.setStudent( data.student, @@ -168,6 +169,8 @@ module.exports = class AttendanceDBApi { transaction, }); + attendance.forEach((record) => assertRecordInCurrentSchool(record, currentUser, 'schoolId')); + await db.sequelize.transaction(async (transaction) => { for (const record of attendance) { await record.update( @@ -189,6 +192,7 @@ module.exports = class AttendanceDBApi { const transaction = (options && options.transaction) || undefined; const attendance = await db.attendance.findByPk(id, options); + assertRecordInCurrentSchool(attendance, currentUser, 'schoolId'); await attendance.update({ deletedBy: currentUser.id @@ -205,6 +209,9 @@ module.exports = class AttendanceDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || null; + + applySchoolScope(where, currentUser?.app_role?.globalAccess, currentUser, 'schoolId'); const attendance = await db.attendance.findOne( { where }, @@ -262,26 +269,11 @@ module.exports = class AttendanceDBApi { let offset = 0; let where = {}; const currentPage = +filter.page; - - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; - - - - if (userSchools) { - if (options?.currentUser?.schoolsId) { - where.schoolsId = options.currentUser.schoolsId; - } - } offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ { @@ -421,11 +413,7 @@ module.exports = class AttendanceDBApi { } - - if (globalAccess) { - delete where.schoolsId; - } - + applySchoolScope(where, globalAccess, user, 'schoolId'); const queryOptions = { where, @@ -456,14 +444,9 @@ module.exports = class AttendanceDBApi { } } - static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { + static async findAllAutocomplete(query, limit, offset, globalAccess, schoolId,) { let where = {}; - - if (!globalAccess && organizationId) { - where.organizationId = organizationId; - } - if (query) { where = { @@ -478,6 +461,8 @@ module.exports = class AttendanceDBApi { }; } + applySchoolScopeById(where, globalAccess, schoolId, 'schoolId'); + const records = await db.attendance.findAll({ attributes: [ 'id', 'observacao' ], where, diff --git a/backend/src/db/api/book_loans.js b/backend/src/db/api/book_loans.js index 6c7d43d..4aedf64 100644 --- a/backend/src/db/api/book_loans.js +++ b/backend/src/db/api/book_loans.js @@ -1,8 +1,7 @@ const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); +const { applySchoolScope, applySchoolScopeById, assertRecordInCurrentSchool, assertRelatedRecordInCurrentSchool, resolveSchoolIdForMutation } = require('./schoolScope'); @@ -54,14 +53,16 @@ module.exports = class Book_loansDBApi { ); - await book_loans.setSchool( data.school || null, { + await book_loans.setSchool(resolveSchoolIdForMutation(data.school, currentUser), { transaction, }); + await assertRelatedRecordInCurrentSchool('books', data.book, currentUser, 'schoolId', { transaction }); await book_loans.setBook( data.book || null, { transaction, }); + await assertRelatedRecordInCurrentSchool('students', data.student, currentUser, 'schoolId', { transaction }); await book_loans.setStudent( data.student || null, { transaction, }); @@ -111,6 +112,7 @@ module.exports = class Book_loansDBApi { importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, + schoolId: resolveSchoolIdForMutation(item.school, currentUser), createdAt: new Date(Date.now() + index * 1000), })); @@ -126,9 +128,8 @@ module.exports = class Book_loansDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; - const book_loans = await db.book_loans.findByPk(id, {}, {transaction}); + assertRecordInCurrentSchool(book_loans, currentUser, 'schoolId'); @@ -159,13 +160,14 @@ module.exports = class Book_loansDBApi { if (data.school !== undefined) { await book_loans.setSchool( - data.school, + resolveSchoolIdForMutation(data.school, currentUser), { transaction } ); } if (data.book !== undefined) { + await assertRelatedRecordInCurrentSchool('books', data.book, currentUser, 'schoolId', { transaction }); await book_loans.setBook( data.book, @@ -175,6 +177,7 @@ module.exports = class Book_loansDBApi { } if (data.student !== undefined) { + await assertRelatedRecordInCurrentSchool('students', data.student, currentUser, 'schoolId', { transaction }); await book_loans.setStudent( data.student, @@ -205,6 +208,8 @@ module.exports = class Book_loansDBApi { transaction, }); + book_loans.forEach((record) => assertRecordInCurrentSchool(record, currentUser, 'schoolId')); + await db.sequelize.transaction(async (transaction) => { for (const record of book_loans) { await record.update( @@ -226,6 +231,7 @@ module.exports = class Book_loansDBApi { const transaction = (options && options.transaction) || undefined; const book_loans = await db.book_loans.findByPk(id, options); + assertRecordInCurrentSchool(book_loans, currentUser, 'schoolId'); await book_loans.update({ deletedBy: currentUser.id @@ -242,6 +248,9 @@ module.exports = class Book_loansDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || null; + + applySchoolScope(where, currentUser?.app_role?.globalAccess, currentUser, 'schoolId'); const book_loans = await db.book_loans.findOne( { where }, @@ -304,26 +313,11 @@ module.exports = class Book_loansDBApi { let offset = 0; let where = {}; const currentPage = +filter.page; - - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; - - - - if (userSchools) { - if (options?.currentUser?.schoolsId) { - where.schoolsId = options.currentUser.schoolsId; - } - } offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ { @@ -561,11 +555,7 @@ module.exports = class Book_loansDBApi { } - - if (globalAccess) { - delete where.schoolsId; - } - + applySchoolScope(where, globalAccess, user, 'schoolId'); const queryOptions = { where, @@ -596,14 +586,9 @@ module.exports = class Book_loansDBApi { } } - static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { + static async findAllAutocomplete(query, limit, offset, globalAccess, schoolId,) { let where = {}; - - if (!globalAccess && organizationId) { - where.organizationId = organizationId; - } - if (query) { where = { @@ -618,6 +603,8 @@ module.exports = class Book_loansDBApi { }; } + applySchoolScopeById(where, globalAccess, schoolId, 'schoolId'); + const records = await db.book_loans.findAll({ attributes: [ 'id', 'status' ], where, diff --git a/backend/src/db/api/books.js b/backend/src/db/api/books.js index 5b52338..7040a14 100644 --- a/backend/src/db/api/books.js +++ b/backend/src/db/api/books.js @@ -1,8 +1,7 @@ const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); +const { applySchoolScope, applySchoolScopeById, assertRecordInCurrentSchool, resolveSchoolIdForMutation } = require('./schoolScope'); @@ -59,7 +58,7 @@ module.exports = class BooksDBApi { ); - await books.setSchool( data.school || null, { + await books.setSchool(resolveSchoolIdForMutation(data.school, currentUser), { transaction, }); @@ -113,6 +112,7 @@ module.exports = class BooksDBApi { importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, + schoolId: resolveSchoolIdForMutation(item.school, currentUser), createdAt: new Date(Date.now() + index * 1000), })); @@ -128,9 +128,8 @@ module.exports = class BooksDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; - const books = await db.books.findByPk(id, {}, {transaction}); + assertRecordInCurrentSchool(books, currentUser, 'schoolId'); @@ -164,7 +163,7 @@ module.exports = class BooksDBApi { if (data.school !== undefined) { await books.setSchool( - data.school, + resolveSchoolIdForMutation(data.school, currentUser), { transaction } ); @@ -192,6 +191,8 @@ module.exports = class BooksDBApi { transaction, }); + books.forEach((record) => assertRecordInCurrentSchool(record, currentUser, 'schoolId')); + await db.sequelize.transaction(async (transaction) => { for (const record of books) { await record.update( @@ -213,6 +214,7 @@ module.exports = class BooksDBApi { const transaction = (options && options.transaction) || undefined; const books = await db.books.findByPk(id, options); + assertRecordInCurrentSchool(books, currentUser, 'schoolId'); await books.update({ deletedBy: currentUser.id @@ -229,6 +231,9 @@ module.exports = class BooksDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || null; + + applySchoolScope(where, currentUser?.app_role?.globalAccess, currentUser, 'schoolId'); const books = await db.books.findOne( { where }, @@ -285,26 +290,11 @@ module.exports = class BooksDBApi { let offset = 0; let where = {}; const currentPage = +filter.page; - - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; - - - - if (userSchools) { - if (options?.currentUser?.schoolsId) { - where.schoolsId = options.currentUser.schoolsId; - } - } offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ { @@ -471,11 +461,7 @@ module.exports = class BooksDBApi { } - - if (globalAccess) { - delete where.schoolsId; - } - + applySchoolScope(where, globalAccess, user, 'schoolId'); const queryOptions = { where, @@ -506,14 +492,9 @@ module.exports = class BooksDBApi { } } - static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { + static async findAllAutocomplete(query, limit, offset, globalAccess, schoolId,) { let where = {}; - - if (!globalAccess && organizationId) { - where.organizationId = organizationId; - } - if (query) { where = { @@ -528,6 +509,8 @@ module.exports = class BooksDBApi { }; } + applySchoolScopeById(where, globalAccess, schoolId, 'schoolId'); + const records = await db.books.findAll({ attributes: [ 'id', 'titulo' ], where, diff --git a/backend/src/db/api/classes.js b/backend/src/db/api/classes.js index 648c885..9583ebf 100644 --- a/backend/src/db/api/classes.js +++ b/backend/src/db/api/classes.js @@ -1,8 +1,7 @@ const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); +const { applySchoolScope, applySchoolScopeById, assertRecordInCurrentSchool, assertRelatedRecordInCurrentSchool, resolveSchoolIdForMutation } = require('./schoolScope'); @@ -54,10 +53,11 @@ module.exports = class ClassesDBApi { ); - await classes.setSchool( data.school || null, { + await classes.setSchool(resolveSchoolIdForMutation(data.school, currentUser), { transaction, }); + await assertRelatedRecordInCurrentSchool('grades', data.grade, currentUser, 'schoolId', { transaction }); await classes.setGrade( data.grade || null, { transaction, }); @@ -107,6 +107,7 @@ module.exports = class ClassesDBApi { importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, + schoolId: resolveSchoolIdForMutation(item.school, currentUser), createdAt: new Date(Date.now() + index * 1000), })); @@ -122,9 +123,8 @@ module.exports = class ClassesDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; - const classes = await db.classes.findByPk(id, {}, {transaction}); + assertRecordInCurrentSchool(classes, currentUser, 'schoolId'); @@ -155,13 +155,14 @@ module.exports = class ClassesDBApi { if (data.school !== undefined) { await classes.setSchool( - data.school, + resolveSchoolIdForMutation(data.school, currentUser), { transaction } ); } if (data.grade !== undefined) { + await assertRelatedRecordInCurrentSchool('grades', data.grade, currentUser, 'schoolId', { transaction }); await classes.setGrade( data.grade, @@ -192,6 +193,8 @@ module.exports = class ClassesDBApi { transaction, }); + classes.forEach((record) => assertRecordInCurrentSchool(record, currentUser, 'schoolId')); + await db.sequelize.transaction(async (transaction) => { for (const record of classes) { await record.update( @@ -213,6 +216,7 @@ module.exports = class ClassesDBApi { const transaction = (options && options.transaction) || undefined; const classes = await db.classes.findByPk(id, options); + assertRecordInCurrentSchool(classes, currentUser, 'schoolId'); await classes.update({ deletedBy: currentUser.id @@ -229,6 +233,9 @@ module.exports = class ClassesDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || null; + + applySchoolScope(where, currentUser?.app_role?.globalAccess, currentUser, 'schoolId'); const classes = await db.classes.findOne( { where }, @@ -290,26 +297,11 @@ module.exports = class ClassesDBApi { let offset = 0; let where = {}; const currentPage = +filter.page; - - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; - - - - if (userSchools) { - if (options?.currentUser?.schoolsId) { - where.schoolsId = options.currentUser.schoolsId; - } - } offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ { @@ -467,11 +459,7 @@ module.exports = class ClassesDBApi { } - - if (globalAccess) { - delete where.schoolsId; - } - + applySchoolScope(where, globalAccess, user, 'schoolId'); const queryOptions = { where, @@ -502,14 +490,9 @@ module.exports = class ClassesDBApi { } } - static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { + static async findAllAutocomplete(query, limit, offset, globalAccess, schoolId,) { let where = {}; - - if (!globalAccess && organizationId) { - where.organizationId = organizationId; - } - if (query) { where = { @@ -524,6 +507,8 @@ module.exports = class ClassesDBApi { }; } + applySchoolScopeById(where, globalAccess, schoolId, 'schoolId'); + const records = await db.classes.findAll({ attributes: [ 'id', 'nome' ], where, diff --git a/backend/src/db/api/courses.js b/backend/src/db/api/courses.js index 880a398..a8a29e1 100644 --- a/backend/src/db/api/courses.js +++ b/backend/src/db/api/courses.js @@ -1,8 +1,7 @@ const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); +const { applySchoolScope, applySchoolScopeById, assertRecordInCurrentSchool, resolveSchoolIdForMutation } = require('./schoolScope'); @@ -44,7 +43,7 @@ module.exports = class CoursesDBApi { ); - await courses.setSchool( data.school || null, { + await courses.setSchool(resolveSchoolIdForMutation(data.school, currentUser), { transaction, }); @@ -83,6 +82,7 @@ module.exports = class CoursesDBApi { importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, + schoolId: resolveSchoolIdForMutation(item.school, currentUser), createdAt: new Date(Date.now() + index * 1000), })); @@ -98,9 +98,8 @@ module.exports = class CoursesDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; - const courses = await db.courses.findByPk(id, {}, {transaction}); + assertRecordInCurrentSchool(courses, currentUser, 'schoolId'); @@ -125,7 +124,7 @@ module.exports = class CoursesDBApi { if (data.school !== undefined) { await courses.setSchool( - data.school, + resolveSchoolIdForMutation(data.school, currentUser), { transaction } ); @@ -153,6 +152,8 @@ module.exports = class CoursesDBApi { transaction, }); + courses.forEach((record) => assertRecordInCurrentSchool(record, currentUser, 'schoolId')); + await db.sequelize.transaction(async (transaction) => { for (const record of courses) { await record.update( @@ -174,6 +175,7 @@ module.exports = class CoursesDBApi { const transaction = (options && options.transaction) || undefined; const courses = await db.courses.findByPk(id, options); + assertRecordInCurrentSchool(courses, currentUser, 'schoolId'); await courses.update({ deletedBy: currentUser.id @@ -190,6 +192,9 @@ module.exports = class CoursesDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || null; + + applySchoolScope(where, currentUser?.app_role?.globalAccess, currentUser, 'schoolId'); const courses = await db.courses.findOne( { where }, @@ -242,26 +247,11 @@ module.exports = class CoursesDBApi { let offset = 0; let where = {}; const currentPage = +filter.page; - - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; - - - - if (userSchools) { - if (options?.currentUser?.schoolsId) { - where.schoolsId = options.currentUser.schoolsId; - } - } offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ { @@ -369,11 +359,7 @@ module.exports = class CoursesDBApi { } - - if (globalAccess) { - delete where.schoolsId; - } - + applySchoolScope(where, globalAccess, user, 'schoolId'); const queryOptions = { where, @@ -404,14 +390,9 @@ module.exports = class CoursesDBApi { } } - static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { + static async findAllAutocomplete(query, limit, offset, globalAccess, schoolId,) { let where = {}; - - if (!globalAccess && organizationId) { - where.organizationId = organizationId; - } - if (query) { where = { @@ -426,6 +407,8 @@ module.exports = class CoursesDBApi { }; } + applySchoolScopeById(where, globalAccess, schoolId, 'schoolId'); + const records = await db.courses.findAll({ attributes: [ 'id', 'nome' ], where, diff --git a/backend/src/db/api/employees.js b/backend/src/db/api/employees.js index 26dd97f..86db230 100644 --- a/backend/src/db/api/employees.js +++ b/backend/src/db/api/employees.js @@ -1,8 +1,7 @@ const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); +const { applySchoolScope, applySchoolScopeById, assertRecordInCurrentSchool, resolveSchoolIdForMutation } = require('./schoolScope'); @@ -59,7 +58,7 @@ module.exports = class EmployeesDBApi { ); - await employees.setSchool( data.school || null, { + await employees.setSchool(resolveSchoolIdForMutation(data.school, currentUser), { transaction, }); @@ -113,6 +112,7 @@ module.exports = class EmployeesDBApi { importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, + schoolId: resolveSchoolIdForMutation(item.school, currentUser), createdAt: new Date(Date.now() + index * 1000), })); @@ -128,9 +128,8 @@ module.exports = class EmployeesDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; - const employees = await db.employees.findByPk(id, {}, {transaction}); + assertRecordInCurrentSchool(employees, currentUser, 'schoolId'); @@ -164,7 +163,7 @@ module.exports = class EmployeesDBApi { if (data.school !== undefined) { await employees.setSchool( - data.school, + resolveSchoolIdForMutation(data.school, currentUser), { transaction } ); @@ -192,6 +191,8 @@ module.exports = class EmployeesDBApi { transaction, }); + employees.forEach((record) => assertRecordInCurrentSchool(record, currentUser, 'schoolId')); + await db.sequelize.transaction(async (transaction) => { for (const record of employees) { await record.update( @@ -213,6 +214,7 @@ module.exports = class EmployeesDBApi { const transaction = (options && options.transaction) || undefined; const employees = await db.employees.findByPk(id, options); + assertRecordInCurrentSchool(employees, currentUser, 'schoolId'); await employees.update({ deletedBy: currentUser.id @@ -229,6 +231,9 @@ module.exports = class EmployeesDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || null; + + applySchoolScope(where, currentUser?.app_role?.globalAccess, currentUser, 'schoolId'); const employees = await db.employees.findOne( { where }, @@ -281,26 +286,11 @@ module.exports = class EmployeesDBApi { let offset = 0; let where = {}; const currentPage = +filter.page; - - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; - - - - if (userSchools) { - if (options?.currentUser?.schoolsId) { - where.schoolsId = options.currentUser.schoolsId; - } - } offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ { @@ -467,11 +457,7 @@ module.exports = class EmployeesDBApi { } - - if (globalAccess) { - delete where.schoolsId; - } - + applySchoolScope(where, globalAccess, user, 'schoolId'); const queryOptions = { where, @@ -502,14 +488,9 @@ module.exports = class EmployeesDBApi { } } - static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { + static async findAllAutocomplete(query, limit, offset, globalAccess, schoolId,) { let where = {}; - - if (!globalAccess && organizationId) { - where.organizationId = organizationId; - } - if (query) { where = { @@ -524,6 +505,8 @@ module.exports = class EmployeesDBApi { }; } + applySchoolScopeById(where, globalAccess, schoolId, 'schoolId'); + const records = await db.employees.findAll({ attributes: [ 'id', 'nome' ], where, diff --git a/backend/src/db/api/enrollments.js b/backend/src/db/api/enrollments.js index 5449858..321c353 100644 --- a/backend/src/db/api/enrollments.js +++ b/backend/src/db/api/enrollments.js @@ -1,8 +1,7 @@ const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); +const { applySchoolScope, applySchoolScopeById, assertRecordInCurrentSchool, assertRelatedRecordInCurrentSchool, resolveSchoolIdForMutation } = require('./schoolScope'); @@ -44,14 +43,16 @@ module.exports = class EnrollmentsDBApi { ); - await enrollments.setSchool( data.school || null, { + await enrollments.setSchool(resolveSchoolIdForMutation(data.school, currentUser), { transaction, }); + await assertRelatedRecordInCurrentSchool('students', data.student, currentUser, 'schoolId', { transaction }); await enrollments.setStudent( data.student || null, { transaction, }); + await assertRelatedRecordInCurrentSchool('classes', data.class, currentUser, 'schoolId', { transaction }); await enrollments.setClass( data.class || null, { transaction, }); @@ -91,6 +92,7 @@ module.exports = class EnrollmentsDBApi { importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, + schoolId: resolveSchoolIdForMutation(item.school, currentUser), createdAt: new Date(Date.now() + index * 1000), })); @@ -106,9 +108,8 @@ module.exports = class EnrollmentsDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; - const enrollments = await db.enrollments.findByPk(id, {}, {transaction}); + assertRecordInCurrentSchool(enrollments, currentUser, 'schoolId'); @@ -133,13 +134,14 @@ module.exports = class EnrollmentsDBApi { if (data.school !== undefined) { await enrollments.setSchool( - data.school, + resolveSchoolIdForMutation(data.school, currentUser), { transaction } ); } if (data.student !== undefined) { + await assertRelatedRecordInCurrentSchool('students', data.student, currentUser, 'schoolId', { transaction }); await enrollments.setStudent( data.student, @@ -149,6 +151,7 @@ module.exports = class EnrollmentsDBApi { } if (data.class !== undefined) { + await assertRelatedRecordInCurrentSchool('classes', data.class, currentUser, 'schoolId', { transaction }); await enrollments.setClass( data.class, @@ -179,6 +182,8 @@ module.exports = class EnrollmentsDBApi { transaction, }); + enrollments.forEach((record) => assertRecordInCurrentSchool(record, currentUser, 'schoolId')); + await db.sequelize.transaction(async (transaction) => { for (const record of enrollments) { await record.update( @@ -200,6 +205,7 @@ module.exports = class EnrollmentsDBApi { const transaction = (options && options.transaction) || undefined; const enrollments = await db.enrollments.findByPk(id, options); + assertRecordInCurrentSchool(enrollments, currentUser, 'schoolId'); await enrollments.update({ deletedBy: currentUser.id @@ -216,6 +222,9 @@ module.exports = class EnrollmentsDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || null; + + applySchoolScope(where, currentUser?.app_role?.globalAccess, currentUser, 'schoolId'); const enrollments = await db.enrollments.findOne( { where }, @@ -278,26 +287,11 @@ module.exports = class EnrollmentsDBApi { let offset = 0; let where = {}; const currentPage = +filter.page; - - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; - - - - if (userSchools) { - if (options?.currentUser?.schoolsId) { - where.schoolsId = options.currentUser.schoolsId; - } - } offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ { @@ -456,11 +450,7 @@ module.exports = class EnrollmentsDBApi { } - - if (globalAccess) { - delete where.schoolsId; - } - + applySchoolScope(where, globalAccess, user, 'schoolId'); const queryOptions = { where, @@ -491,14 +481,9 @@ module.exports = class EnrollmentsDBApi { } } - static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { + static async findAllAutocomplete(query, limit, offset, globalAccess, schoolId,) { let where = {}; - - if (!globalAccess && organizationId) { - where.organizationId = organizationId; - } - if (query) { where = { @@ -513,6 +498,8 @@ module.exports = class EnrollmentsDBApi { }; } + applySchoolScopeById(where, globalAccess, schoolId, 'schoolId'); + const records = await db.enrollments.findAll({ attributes: [ 'id', 'ano_lectivo' ], where, diff --git a/backend/src/db/api/file.js b/backend/src/db/api/file.js index f4f7121..d2eae1c 100644 --- a/backend/src/db/api/file.js +++ b/backend/src/db/api/file.js @@ -36,15 +36,17 @@ module.exports = class FileDBApi { ); for (const file of inexistentFiles) { + const safeFile = await services.validateFileMetadata(file, relation, { transaction }); + await db.file.create( { belongsTo: relation.belongsTo, belongsToColumn: relation.belongsToColumn, belongsToId: relation.belongsToId, - name: file.name, - sizeInBytes: file.sizeInBytes, - privateUrl: file.privateUrl, - publicUrl: file.publicUrl, + name: safeFile.name, + sizeInBytes: safeFile.sizeInBytes, + privateUrl: safeFile.privateUrl, + publicUrl: safeFile.publicUrl, createdById: currentUser.id, updatedById: currentUser.id, }, diff --git a/backend/src/db/api/grades.js b/backend/src/db/api/grades.js index c38c8a2..46d6329 100644 --- a/backend/src/db/api/grades.js +++ b/backend/src/db/api/grades.js @@ -1,8 +1,7 @@ const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); +const { applySchoolScope, applySchoolScopeById, assertRecordInCurrentSchool, resolveSchoolIdForMutation } = require('./schoolScope'); @@ -44,7 +43,7 @@ module.exports = class GradesDBApi { ); - await grades.setSchool( data.school || null, { + await grades.setSchool(resolveSchoolIdForMutation(data.school, currentUser), { transaction, }); @@ -83,6 +82,7 @@ module.exports = class GradesDBApi { importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, + schoolId: resolveSchoolIdForMutation(item.school, currentUser), createdAt: new Date(Date.now() + index * 1000), })); @@ -98,9 +98,8 @@ module.exports = class GradesDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; - const grades = await db.grades.findByPk(id, {}, {transaction}); + assertRecordInCurrentSchool(grades, currentUser, 'schoolId'); @@ -125,7 +124,7 @@ module.exports = class GradesDBApi { if (data.school !== undefined) { await grades.setSchool( - data.school, + resolveSchoolIdForMutation(data.school, currentUser), { transaction } ); @@ -153,6 +152,8 @@ module.exports = class GradesDBApi { transaction, }); + grades.forEach((record) => assertRecordInCurrentSchool(record, currentUser, 'schoolId')); + await db.sequelize.transaction(async (transaction) => { for (const record of grades) { await record.update( @@ -174,6 +175,7 @@ module.exports = class GradesDBApi { const transaction = (options && options.transaction) || undefined; const grades = await db.grades.findByPk(id, options); + assertRecordInCurrentSchool(grades, currentUser, 'schoolId'); await grades.update({ deletedBy: currentUser.id @@ -190,6 +192,9 @@ module.exports = class GradesDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || null; + + applySchoolScope(where, currentUser?.app_role?.globalAccess, currentUser, 'schoolId'); const grades = await db.grades.findOne( { where }, @@ -246,26 +251,11 @@ module.exports = class GradesDBApi { let offset = 0; let where = {}; const currentPage = +filter.page; - - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; - - - - if (userSchools) { - if (options?.currentUser?.schoolsId) { - where.schoolsId = options.currentUser.schoolsId; - } - } offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ { @@ -386,11 +376,7 @@ module.exports = class GradesDBApi { } - - if (globalAccess) { - delete where.schoolsId; - } - + applySchoolScope(where, globalAccess, user, 'schoolId'); const queryOptions = { where, @@ -421,14 +407,9 @@ module.exports = class GradesDBApi { } } - static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { + static async findAllAutocomplete(query, limit, offset, globalAccess, schoolId,) { let where = {}; - - if (!globalAccess && organizationId) { - where.organizationId = organizationId; - } - if (query) { where = { @@ -443,6 +424,8 @@ module.exports = class GradesDBApi { }; } + applySchoolScopeById(where, globalAccess, schoolId, 'schoolId'); + const records = await db.grades.findAll({ attributes: [ 'id', 'nome' ], where, diff --git a/backend/src/db/api/guardians.js b/backend/src/db/api/guardians.js index 6050d38..c76dcde 100644 --- a/backend/src/db/api/guardians.js +++ b/backend/src/db/api/guardians.js @@ -1,8 +1,7 @@ const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); +const { applySchoolScope, applySchoolScopeById, assertRecordInCurrentSchool, resolveSchoolIdForMutation } = require('./schoolScope'); @@ -49,7 +48,7 @@ module.exports = class GuardiansDBApi { ); - await guardians.setSchool( data.school || null, { + await guardians.setSchool(resolveSchoolIdForMutation(data.school, currentUser), { transaction, }); @@ -93,6 +92,7 @@ module.exports = class GuardiansDBApi { importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, + schoolId: resolveSchoolIdForMutation(item.school, currentUser), createdAt: new Date(Date.now() + index * 1000), })); @@ -108,9 +108,8 @@ module.exports = class GuardiansDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; - const guardians = await db.guardians.findByPk(id, {}, {transaction}); + assertRecordInCurrentSchool(guardians, currentUser, 'schoolId'); @@ -138,7 +137,7 @@ module.exports = class GuardiansDBApi { if (data.school !== undefined) { await guardians.setSchool( - data.school, + resolveSchoolIdForMutation(data.school, currentUser), { transaction } ); @@ -166,6 +165,8 @@ module.exports = class GuardiansDBApi { transaction, }); + guardians.forEach((record) => assertRecordInCurrentSchool(record, currentUser, 'schoolId')); + await db.sequelize.transaction(async (transaction) => { for (const record of guardians) { await record.update( @@ -187,6 +188,7 @@ module.exports = class GuardiansDBApi { const transaction = (options && options.transaction) || undefined; const guardians = await db.guardians.findByPk(id, options); + assertRecordInCurrentSchool(guardians, currentUser, 'schoolId'); await guardians.update({ deletedBy: currentUser.id @@ -203,6 +205,9 @@ module.exports = class GuardiansDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || null; + + applySchoolScope(where, currentUser?.app_role?.globalAccess, currentUser, 'schoolId'); const guardians = await db.guardians.findOne( { where }, @@ -259,26 +264,11 @@ module.exports = class GuardiansDBApi { let offset = 0; let where = {}; const currentPage = +filter.page; - - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; - - - - if (userSchools) { - if (options?.currentUser?.schoolsId) { - where.schoolsId = options.currentUser.schoolsId; - } - } offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ { @@ -397,11 +387,7 @@ module.exports = class GuardiansDBApi { } - - if (globalAccess) { - delete where.schoolsId; - } - + applySchoolScope(where, globalAccess, user, 'schoolId'); const queryOptions = { where, @@ -432,14 +418,9 @@ module.exports = class GuardiansDBApi { } } - static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { + static async findAllAutocomplete(query, limit, offset, globalAccess, schoolId,) { let where = {}; - - if (!globalAccess && organizationId) { - where.organizationId = organizationId; - } - if (query) { where = { @@ -454,6 +435,8 @@ module.exports = class GuardiansDBApi { }; } + applySchoolScopeById(where, globalAccess, schoolId, 'schoolId'); + const records = await db.guardians.findAll({ attributes: [ 'id', 'nome' ], where, diff --git a/backend/src/db/api/invoices.js b/backend/src/db/api/invoices.js index 0fae66a..4ea7c20 100644 --- a/backend/src/db/api/invoices.js +++ b/backend/src/db/api/invoices.js @@ -1,8 +1,7 @@ const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); +const { applySchoolScope, applySchoolScopeById, assertRecordInCurrentSchool, assertRelatedRecordInCurrentSchool, resolveSchoolIdForMutation } = require('./schoolScope'); @@ -59,10 +58,11 @@ module.exports = class InvoicesDBApi { ); - await invoices.setSchool( data.school || null, { + await invoices.setSchool(resolveSchoolIdForMutation(data.school, currentUser), { transaction, }); + await assertRelatedRecordInCurrentSchool('students', data.student, currentUser, 'schoolId', { transaction }); await invoices.setStudent( data.student || null, { transaction, }); @@ -117,6 +117,7 @@ module.exports = class InvoicesDBApi { importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, + schoolId: resolveSchoolIdForMutation(item.school, currentUser), createdAt: new Date(Date.now() + index * 1000), })); @@ -132,9 +133,8 @@ module.exports = class InvoicesDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; - const invoices = await db.invoices.findByPk(id, {}, {transaction}); + assertRecordInCurrentSchool(invoices, currentUser, 'schoolId'); @@ -168,13 +168,14 @@ module.exports = class InvoicesDBApi { if (data.school !== undefined) { await invoices.setSchool( - data.school, + resolveSchoolIdForMutation(data.school, currentUser), { transaction } ); } if (data.student !== undefined) { + await assertRelatedRecordInCurrentSchool('students', data.student, currentUser, 'schoolId', { transaction }); await invoices.setStudent( data.student, @@ -205,6 +206,8 @@ module.exports = class InvoicesDBApi { transaction, }); + invoices.forEach((record) => assertRecordInCurrentSchool(record, currentUser, 'schoolId')); + await db.sequelize.transaction(async (transaction) => { for (const record of invoices) { await record.update( @@ -226,6 +229,7 @@ module.exports = class InvoicesDBApi { const transaction = (options && options.transaction) || undefined; const invoices = await db.invoices.findByPk(id, options); + assertRecordInCurrentSchool(invoices, currentUser, 'schoolId'); await invoices.update({ deletedBy: currentUser.id @@ -242,6 +246,9 @@ module.exports = class InvoicesDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || null; + + applySchoolScope(where, currentUser?.app_role?.globalAccess, currentUser, 'schoolId'); const invoices = await db.invoices.findOne( { where }, @@ -303,26 +310,11 @@ module.exports = class InvoicesDBApi { let offset = 0; let where = {}; const currentPage = +filter.page; - - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; - - - - if (userSchools) { - if (options?.currentUser?.schoolsId) { - where.schoolsId = options.currentUser.schoolsId; - } - } offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ { @@ -521,11 +513,7 @@ module.exports = class InvoicesDBApi { } - - if (globalAccess) { - delete where.schoolsId; - } - + applySchoolScope(where, globalAccess, user, 'schoolId'); const queryOptions = { where, @@ -556,14 +544,9 @@ module.exports = class InvoicesDBApi { } } - static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { + static async findAllAutocomplete(query, limit, offset, globalAccess, schoolId,) { let where = {}; - - if (!globalAccess && organizationId) { - where.organizationId = organizationId; - } - if (query) { where = { @@ -578,6 +561,8 @@ module.exports = class InvoicesDBApi { }; } + applySchoolScopeById(where, globalAccess, schoolId, 'schoolId'); + const records = await db.invoices.findAll({ attributes: [ 'id', 'referencia' ], where, diff --git a/backend/src/db/api/payments.js b/backend/src/db/api/payments.js index f6dd695..dc393cd 100644 --- a/backend/src/db/api/payments.js +++ b/backend/src/db/api/payments.js @@ -1,8 +1,8 @@ const db = require('../models'); const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); +const { applySchoolScope, applySchoolScopeById, assertRecordInCurrentSchool, assertRelatedRecordInCurrentSchool, resolveSchoolIdForMutation } = require('./schoolScope'); @@ -49,10 +49,11 @@ module.exports = class PaymentsDBApi { ); - await payments.setSchool( data.school || null, { + await payments.setSchool(resolveSchoolIdForMutation(data.school, currentUser), { transaction, }); + await assertRelatedRecordInCurrentSchool('invoices', data.invoice, currentUser, 'schoolId', { transaction }); await payments.setInvoice( data.invoice || null, { transaction, }); @@ -107,6 +108,7 @@ module.exports = class PaymentsDBApi { importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, + schoolId: resolveSchoolIdForMutation(item.school, currentUser), createdAt: new Date(Date.now() + index * 1000), })); @@ -134,9 +136,8 @@ module.exports = class PaymentsDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; - const payments = await db.payments.findByPk(id, {}, {transaction}); + assertRecordInCurrentSchool(payments, currentUser, 'schoolId'); @@ -164,13 +165,14 @@ module.exports = class PaymentsDBApi { if (data.school !== undefined) { await payments.setSchool( - data.school, + resolveSchoolIdForMutation(data.school, currentUser), { transaction } ); } if (data.invoice !== undefined) { + await assertRelatedRecordInCurrentSchool('invoices', data.invoice, currentUser, 'schoolId', { transaction }); await payments.setInvoice( data.invoice, @@ -184,15 +186,17 @@ module.exports = class PaymentsDBApi { - await FileDBApi.replaceRelationFiles( - { - belongsTo: db.payments.getTableName(), - belongsToColumn: 'comprovativo', - belongsToId: payments.id, - }, - data.comprovativo, - options, - ); + if (data.comprovativo !== undefined) { + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.payments.getTableName(), + belongsToColumn: 'comprovativo', + belongsToId: payments.id, + }, + data.comprovativo, + options, + ); + } return payments; @@ -211,6 +215,8 @@ module.exports = class PaymentsDBApi { transaction, }); + payments.forEach((record) => assertRecordInCurrentSchool(record, currentUser, 'schoolId')); + await db.sequelize.transaction(async (transaction) => { for (const record of payments) { await record.update( @@ -232,6 +238,7 @@ module.exports = class PaymentsDBApi { const transaction = (options && options.transaction) || undefined; const payments = await db.payments.findByPk(id, options); + assertRecordInCurrentSchool(payments, currentUser, 'schoolId'); await payments.update({ deletedBy: currentUser.id @@ -248,6 +255,9 @@ module.exports = class PaymentsDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || null; + + applySchoolScope(where, currentUser?.app_role?.globalAccess, currentUser, 'schoolId'); const payments = await db.payments.findOne( { where }, @@ -310,26 +320,11 @@ module.exports = class PaymentsDBApi { let offset = 0; let where = {}; const currentPage = +filter.page; - - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; - - - - if (userSchools) { - if (options?.currentUser?.schoolsId) { - where.schoolsId = options.currentUser.schoolsId; - } - } offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ { @@ -498,11 +493,7 @@ module.exports = class PaymentsDBApi { } - - if (globalAccess) { - delete where.schoolsId; - } - + applySchoolScope(where, globalAccess, user, 'schoolId'); const queryOptions = { where, @@ -533,14 +524,9 @@ module.exports = class PaymentsDBApi { } } - static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { + static async findAllAutocomplete(query, limit, offset, globalAccess, schoolId,) { let where = {}; - - if (!globalAccess && organizationId) { - where.organizationId = organizationId; - } - if (query) { where = { @@ -555,6 +541,8 @@ module.exports = class PaymentsDBApi { }; } + applySchoolScopeById(where, globalAccess, schoolId, 'schoolId'); + const records = await db.payments.findAll({ attributes: [ 'id', 'referencia_transacao' ], where, diff --git a/backend/src/db/api/permissions.js b/backend/src/db/api/permissions.js index 536e472..c0dd7a1 100644 --- a/backend/src/db/api/permissions.js +++ b/backend/src/db/api/permissions.js @@ -1,14 +1,20 @@ const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); +const ForbiddenError = require('../../services/notifications/errors/forbidden'); const Sequelize = db.Sequelize; const Op = Sequelize.Op; + +function assertGlobalAccess(currentUser) { + if (!currentUser?.app_role?.globalAccess) { + throw new ForbiddenError('auth.forbidden'); + } +} + module.exports = class PermissionsDBApi { @@ -16,6 +22,7 @@ module.exports = class PermissionsDBApi { static async create(data, options) { const currentUser = (options && options.currentUser) || { id: null }; const transaction = (options && options.transaction) || undefined; + assertGlobalAccess(currentUser); const permissions = await db.permissions.create( { @@ -46,6 +53,7 @@ module.exports = class PermissionsDBApi { static async bulkImport(data, options) { const currentUser = (options && options.currentUser) || { id: null }; const transaction = (options && options.transaction) || undefined; + assertGlobalAccess(currentUser); // Prepare data - wrapping individual data transformations in a map() method const permissionsData = data.map((item, index) => ({ @@ -74,7 +82,7 @@ module.exports = class PermissionsDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; + assertGlobalAccess(currentUser); const permissions = await db.permissions.findByPk(id, {}, {transaction}); @@ -104,6 +112,7 @@ module.exports = class PermissionsDBApi { static async deleteByIds(ids, options) { const currentUser = (options && options.currentUser) || { id: null }; const transaction = (options && options.transaction) || undefined; + assertGlobalAccess(currentUser); const permissions = await db.permissions.findAll({ where: { @@ -133,6 +142,7 @@ module.exports = class PermissionsDBApi { static async remove(id, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; + assertGlobalAccess(currentUser); const permissions = await db.permissions.findByPk(id, options); @@ -200,18 +210,12 @@ module.exports = class PermissionsDBApi { const currentPage = +filter.page; - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ diff --git a/backend/src/db/api/products.js b/backend/src/db/api/products.js index 3eb7665..dd38717 100644 --- a/backend/src/db/api/products.js +++ b/backend/src/db/api/products.js @@ -1,8 +1,7 @@ const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); +const { applySchoolScope, applySchoolScopeById, assertRecordInCurrentSchool, resolveSchoolIdForMutation } = require('./schoolScope'); @@ -54,7 +53,7 @@ module.exports = class ProductsDBApi { ); - await products.setSchool( data.school || null, { + await products.setSchool(resolveSchoolIdForMutation(data.school, currentUser), { transaction, }); @@ -103,6 +102,7 @@ module.exports = class ProductsDBApi { importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, + schoolId: resolveSchoolIdForMutation(item.school, currentUser), createdAt: new Date(Date.now() + index * 1000), })); @@ -118,9 +118,8 @@ module.exports = class ProductsDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; - const products = await db.products.findByPk(id, {}, {transaction}); + assertRecordInCurrentSchool(products, currentUser, 'schoolId'); @@ -151,7 +150,7 @@ module.exports = class ProductsDBApi { if (data.school !== undefined) { await products.setSchool( - data.school, + resolveSchoolIdForMutation(data.school, currentUser), { transaction } ); @@ -179,6 +178,8 @@ module.exports = class ProductsDBApi { transaction, }); + products.forEach((record) => assertRecordInCurrentSchool(record, currentUser, 'schoolId')); + await db.sequelize.transaction(async (transaction) => { for (const record of products) { await record.update( @@ -200,6 +201,7 @@ module.exports = class ProductsDBApi { const transaction = (options && options.transaction) || undefined; const products = await db.products.findByPk(id, options); + assertRecordInCurrentSchool(products, currentUser, 'schoolId'); await products.update({ deletedBy: currentUser.id @@ -216,6 +218,9 @@ module.exports = class ProductsDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || null; + + applySchoolScope(where, currentUser?.app_role?.globalAccess, currentUser, 'schoolId'); const products = await db.products.findOne( { where }, @@ -268,26 +273,11 @@ module.exports = class ProductsDBApi { let offset = 0; let where = {}; const currentPage = +filter.page; - - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; - - - - if (userSchools) { - if (options?.currentUser?.schoolsId) { - where.schoolsId = options.currentUser.schoolsId; - } - } offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ { @@ -456,11 +446,7 @@ module.exports = class ProductsDBApi { } - - if (globalAccess) { - delete where.schoolsId; - } - + applySchoolScope(where, globalAccess, user, 'schoolId'); const queryOptions = { where, @@ -491,14 +477,9 @@ module.exports = class ProductsDBApi { } } - static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { + static async findAllAutocomplete(query, limit, offset, globalAccess, schoolId,) { let where = {}; - - if (!globalAccess && organizationId) { - where.organizationId = organizationId; - } - if (query) { where = { @@ -513,6 +494,8 @@ module.exports = class ProductsDBApi { }; } + applySchoolScopeById(where, globalAccess, schoolId, 'schoolId'); + const records = await db.products.findAll({ attributes: [ 'id', 'nome' ], where, diff --git a/backend/src/db/api/roles.js b/backend/src/db/api/roles.js index b1bd0fc..4540b0b 100644 --- a/backend/src/db/api/roles.js +++ b/backend/src/db/api/roles.js @@ -1,8 +1,7 @@ const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); +const ForbiddenError = require('../../services/notifications/errors/forbidden'); const config = require('../../config'); @@ -11,6 +10,13 @@ const config = require('../../config'); const Sequelize = db.Sequelize; const Op = Sequelize.Op; + +function assertGlobalAccess(currentUser) { + if (!currentUser?.app_role?.globalAccess) { + throw new ForbiddenError('auth.forbidden'); + } +} + module.exports = class RolesDBApi { @@ -18,6 +24,7 @@ module.exports = class RolesDBApi { static async create(data, options) { const currentUser = (options && options.currentUser) || { id: null }; const transaction = (options && options.transaction) || undefined; + assertGlobalAccess(currentUser); const roles = await db.roles.create( { @@ -63,6 +70,7 @@ module.exports = class RolesDBApi { static async bulkImport(data, options) { const currentUser = (options && options.currentUser) || { id: null }; const transaction = (options && options.transaction) || undefined; + assertGlobalAccess(currentUser); // Prepare data - wrapping individual data transformations in a map() method const rolesData = data.map((item, index) => ({ @@ -102,7 +110,7 @@ module.exports = class RolesDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; + assertGlobalAccess(currentUser); const roles = await db.roles.findByPk(id, {}, {transaction}); @@ -142,6 +150,7 @@ module.exports = class RolesDBApi { static async deleteByIds(ids, options) { const currentUser = (options && options.currentUser) || { id: null }; const transaction = (options && options.transaction) || undefined; + assertGlobalAccess(currentUser); const roles = await db.roles.findAll({ where: { @@ -171,6 +180,7 @@ module.exports = class RolesDBApi { static async remove(id, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; + assertGlobalAccess(currentUser); const roles = await db.roles.findByPk(id, options); @@ -247,18 +257,12 @@ module.exports = class RolesDBApi { const currentPage = +filter.page; - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ diff --git a/backend/src/db/api/schoolScope.js b/backend/src/db/api/schoolScope.js new file mode 100644 index 0000000..d0b06db --- /dev/null +++ b/backend/src/db/api/schoolScope.js @@ -0,0 +1,103 @@ +const ForbiddenError = require('../../services/notifications/errors/forbidden'); +const db = require('../models'); + +function getCurrentUserSchoolId(currentUser) { + return currentUser?.schoolsId || currentUser?.schools?.id || null; +} + +function applySchoolScopeById(where, globalAccess, schoolId, field = 'schoolId') { + if (globalAccess) { + return where; + } + + if (!schoolId) { + where.id = null; + return where; + } + + if (field === 'id' && where.id && String(where.id) !== String(schoolId)) { + where.id = null; + return where; + } + + where[field] = schoolId; + return where; +} + +function applySchoolScope(where, globalAccess, currentUser, field = 'schoolId') { + if (!currentUser || globalAccess) { + return where; + } + + return applySchoolScopeById(where, false, getCurrentUserSchoolId(currentUser), field); +} + + +function resolveSchoolIdForMutation(inputSchoolId, currentUser) { + if (!currentUser || currentUser?.app_role?.globalAccess) { + return inputSchoolId || null; + } + + const schoolId = getCurrentUserSchoolId(currentUser); + if (!schoolId) { + throw new ForbiddenError('auth.forbidden'); + } + + return schoolId; +} + +function assertRecordInCurrentSchool(record, currentUser, field = 'schoolId') { + if (!record || !currentUser || currentUser?.app_role?.globalAccess) { + return; + } + + const schoolId = getCurrentUserSchoolId(currentUser); + const recordSchoolId = field === 'id' ? record.id : record[field]; + + if (!schoolId || String(recordSchoolId || '') !== String(schoolId)) { + throw new ForbiddenError('auth.forbidden'); + } +} + +async function assertRelatedRecordInCurrentSchool(modelName, id, currentUser, field = 'schoolId', options = {}) { + if (!id || !currentUser || currentUser?.app_role?.globalAccess) { + return; + } + + const model = db[modelName]; + if (!model) { + throw new Error(`School scope model not found: ${modelName}`); + } + + if (field === 'id') { + const schoolId = getCurrentUserSchoolId(currentUser); + if (!schoolId || String(id) !== String(schoolId)) { + throw new ForbiddenError('auth.forbidden'); + } + return; + } + + if (!model.rawAttributes?.[field]) { + throw new Error(`School scope field ${field} not found on model ${modelName}`); + } + + const record = await model.findByPk(id, { + attributes: ['id', field], + transaction: options.transaction, + }); + + if (!record) { + throw new ForbiddenError('auth.forbidden'); + } + + assertRecordInCurrentSchool(record, currentUser, field); +} + +module.exports = { + applySchoolScope, + applySchoolScopeById, + assertRecordInCurrentSchool, + assertRelatedRecordInCurrentSchool, + resolveSchoolIdForMutation, + getCurrentUserSchoolId, +}; diff --git a/backend/src/db/api/schools.js b/backend/src/db/api/schools.js index 6b0ab6e..bd714cb 100644 --- a/backend/src/db/api/schools.js +++ b/backend/src/db/api/schools.js @@ -1,6 +1,7 @@ const db = require('../models'); const Utils = require('../utils'); +const { applySchoolScope, applySchoolScopeById, assertRecordInCurrentSchool } = require('./schoolScope'); @@ -89,6 +90,7 @@ module.exports = class SchoolsDBApi { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; const schools = await db.schools.findByPk(id, {}, {transaction}); + assertRecordInCurrentSchool(schools, currentUser, 'id'); @@ -134,6 +136,8 @@ module.exports = class SchoolsDBApi { transaction, }); + schools.forEach((record) => assertRecordInCurrentSchool(record, currentUser, 'id')); + await db.sequelize.transaction(async (transaction) => { for (const record of schools) { await record.update( @@ -155,6 +159,7 @@ module.exports = class SchoolsDBApi { const transaction = (options && options.transaction) || undefined; const schools = await db.schools.findByPk(id, options); + assertRecordInCurrentSchool(schools, currentUser, 'id'); await schools.update({ deletedBy: currentUser.id @@ -171,6 +176,9 @@ module.exports = class SchoolsDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || null; + + applySchoolScope(where, currentUser?.app_role?.globalAccess, currentUser, 'id'); const schools = await db.schools.findOne( { where }, @@ -290,18 +298,7 @@ module.exports = class SchoolsDBApi { let offset = 0; let where = {}; const currentPage = +filter.page; - - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; - - - - if (userSchools) { - if (options?.currentUser?.schoolsId) { - where.schoolsId = options.currentUser.schoolsId; - } - } offset = currentPage * limit; @@ -394,11 +391,7 @@ module.exports = class SchoolsDBApi { } - - if (globalAccess) { - delete where.schoolsId; - } - + applySchoolScope(where, globalAccess, user, 'id'); const queryOptions = { where, @@ -429,14 +422,9 @@ module.exports = class SchoolsDBApi { } } - static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { + static async findAllAutocomplete(query, limit, offset, globalAccess, schoolId,) { let where = {}; - - if (!globalAccess && organizationId) { - where.organizationId = organizationId; - } - if (query) { where = { @@ -451,6 +439,8 @@ module.exports = class SchoolsDBApi { }; } + applySchoolScopeById(where, globalAccess, schoolId, 'id'); + const records = await db.schools.findAll({ attributes: [ 'id', 'name' ], where, diff --git a/backend/src/db/api/student_guardians.js b/backend/src/db/api/student_guardians.js index d02cadd..0c221d3 100644 --- a/backend/src/db/api/student_guardians.js +++ b/backend/src/db/api/student_guardians.js @@ -1,8 +1,7 @@ const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); +const { applySchoolScope, applySchoolScopeById, assertRecordInCurrentSchool, assertRelatedRecordInCurrentSchool, resolveSchoolIdForMutation } = require('./schoolScope'); @@ -40,15 +39,17 @@ module.exports = class Student_guardiansDBApi { ); + await assertRelatedRecordInCurrentSchool('students', data.student, currentUser, 'schoolId', { transaction }); await student_guardians.setStudent( data.student || null, { transaction, }); + await assertRelatedRecordInCurrentSchool('guardians', data.guardian, currentUser, 'schoolId', { transaction }); await student_guardians.setGuardian( data.guardian || null, { transaction, }); - await student_guardians.setSchools( data.schools || null, { + await student_guardians.setSchools(resolveSchoolIdForMutation(data.schools, currentUser), { transaction, }); @@ -83,6 +84,7 @@ module.exports = class Student_guardiansDBApi { importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, + schoolsId: resolveSchoolIdForMutation(item.schools, currentUser), createdAt: new Date(Date.now() + index * 1000), })); @@ -98,9 +100,8 @@ module.exports = class Student_guardiansDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; - const student_guardians = await db.student_guardians.findByPk(id, {}, {transaction}); + assertRecordInCurrentSchool(student_guardians, currentUser, 'schoolsId'); @@ -120,6 +121,7 @@ module.exports = class Student_guardiansDBApi { if (data.student !== undefined) { + await assertRelatedRecordInCurrentSchool('students', data.student, currentUser, 'schoolId', { transaction }); await student_guardians.setStudent( data.student, @@ -129,6 +131,7 @@ module.exports = class Student_guardiansDBApi { } if (data.guardian !== undefined) { + await assertRelatedRecordInCurrentSchool('guardians', data.guardian, currentUser, 'schoolId', { transaction }); await student_guardians.setGuardian( data.guardian, @@ -140,7 +143,7 @@ module.exports = class Student_guardiansDBApi { if (data.schools !== undefined) { await student_guardians.setSchools( - data.schools, + resolveSchoolIdForMutation(data.schools, currentUser), { transaction } ); @@ -168,6 +171,8 @@ module.exports = class Student_guardiansDBApi { transaction, }); + student_guardians.forEach((record) => assertRecordInCurrentSchool(record, currentUser, 'schoolsId')); + await db.sequelize.transaction(async (transaction) => { for (const record of student_guardians) { await record.update( @@ -189,6 +194,7 @@ module.exports = class Student_guardiansDBApi { const transaction = (options && options.transaction) || undefined; const student_guardians = await db.student_guardians.findByPk(id, options); + assertRecordInCurrentSchool(student_guardians, currentUser, 'schoolsId'); await student_guardians.update({ deletedBy: currentUser.id @@ -205,6 +211,9 @@ module.exports = class Student_guardiansDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || null; + + applySchoolScope(where, currentUser?.app_role?.globalAccess, currentUser, 'schoolsId'); const student_guardians = await db.student_guardians.findOne( { where }, @@ -267,26 +276,11 @@ module.exports = class Student_guardiansDBApi { let offset = 0; let where = {}; const currentPage = +filter.page; - - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; - - - - if (userSchools) { - if (options?.currentUser?.schoolsId) { - where.schoolsId = options.currentUser.schoolsId; - } - } offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ { @@ -417,11 +411,7 @@ module.exports = class Student_guardiansDBApi { } - - if (globalAccess) { - delete where.schoolsId; - } - + applySchoolScope(where, globalAccess, user, 'schoolsId'); const queryOptions = { where, @@ -452,14 +442,9 @@ module.exports = class Student_guardiansDBApi { } } - static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { + static async findAllAutocomplete(query, limit, offset, globalAccess, schoolId,) { let where = {}; - - if (!globalAccess && organizationId) { - where.organizationId = organizationId; - } - if (query) { where = { @@ -474,6 +459,8 @@ module.exports = class Student_guardiansDBApi { }; } + applySchoolScopeById(where, globalAccess, schoolId, 'schoolsId'); + const records = await db.student_guardians.findAll({ attributes: [ 'id', 'tipo_responsabilidade' ], where, diff --git a/backend/src/db/api/students.js b/backend/src/db/api/students.js index 85b1bce..712ef23 100644 --- a/backend/src/db/api/students.js +++ b/backend/src/db/api/students.js @@ -1,8 +1,8 @@ const db = require('../models'); const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); +const { applySchoolScope, applySchoolScopeById, assertRecordInCurrentSchool, resolveSchoolIdForMutation } = require('./schoolScope'); @@ -74,7 +74,7 @@ module.exports = class StudentsDBApi { ); - await students.setSchool( data.school || null, { + await students.setSchool(resolveSchoolIdForMutation(data.school, currentUser), { transaction, }); @@ -153,6 +153,7 @@ module.exports = class StudentsDBApi { importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, + schoolId: resolveSchoolIdForMutation(item.school, currentUser), createdAt: new Date(Date.now() + index * 1000), })); @@ -180,9 +181,8 @@ module.exports = class StudentsDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; - const students = await db.students.findByPk(id, {}, {transaction}); + assertRecordInCurrentSchool(students, currentUser, 'schoolId'); @@ -225,7 +225,7 @@ module.exports = class StudentsDBApi { if (data.school !== undefined) { await students.setSchool( - data.school, + resolveSchoolIdForMutation(data.school, currentUser), { transaction } ); @@ -236,15 +236,17 @@ module.exports = class StudentsDBApi { - await FileDBApi.replaceRelationFiles( - { - belongsTo: db.students.getTableName(), - belongsToColumn: 'foto', - belongsToId: students.id, - }, - data.foto, - options, - ); + if (data.foto !== undefined) { + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.students.getTableName(), + belongsToColumn: 'foto', + belongsToId: students.id, + }, + data.foto, + options, + ); + } return students; @@ -263,6 +265,8 @@ module.exports = class StudentsDBApi { transaction, }); + students.forEach((record) => assertRecordInCurrentSchool(record, currentUser, 'schoolId')); + await db.sequelize.transaction(async (transaction) => { for (const record of students) { await record.update( @@ -284,6 +288,7 @@ module.exports = class StudentsDBApi { const transaction = (options && options.transaction) || undefined; const students = await db.students.findByPk(id, options); + assertRecordInCurrentSchool(students, currentUser, 'schoolId'); await students.update({ deletedBy: currentUser.id @@ -300,6 +305,9 @@ module.exports = class StudentsDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || null; + + applySchoolScope(where, currentUser?.app_role?.globalAccess, currentUser, 'schoolId'); const students = await db.students.findOne( { where }, @@ -381,26 +389,11 @@ module.exports = class StudentsDBApi { let offset = 0; let where = {}; const currentPage = +filter.page; - - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; - - - - if (userSchools) { - if (options?.currentUser?.schoolsId) { - where.schoolsId = options.currentUser.schoolsId; - } - } offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ { @@ -588,11 +581,7 @@ module.exports = class StudentsDBApi { } - - if (globalAccess) { - delete where.schoolsId; - } - + applySchoolScope(where, globalAccess, user, 'schoolId'); const queryOptions = { where, @@ -623,14 +612,9 @@ module.exports = class StudentsDBApi { } } - static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { + static async findAllAutocomplete(query, limit, offset, globalAccess, schoolId,) { let where = {}; - - if (!globalAccess && organizationId) { - where.organizationId = organizationId; - } - if (query) { where = { @@ -645,6 +629,8 @@ module.exports = class StudentsDBApi { }; } + applySchoolScopeById(where, globalAccess, schoolId, 'schoolId'); + const records = await db.students.findAll({ attributes: [ 'id', 'nome_completo' ], where, diff --git a/backend/src/db/api/subjects.js b/backend/src/db/api/subjects.js index 842ea07..fbc55f3 100644 --- a/backend/src/db/api/subjects.js +++ b/backend/src/db/api/subjects.js @@ -1,8 +1,7 @@ const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); +const { applySchoolScope, applySchoolScopeById, assertRecordInCurrentSchool, resolveSchoolIdForMutation } = require('./schoolScope'); @@ -39,7 +38,7 @@ module.exports = class SubjectsDBApi { ); - await subjects.setSchool( data.school || null, { + await subjects.setSchool(resolveSchoolIdForMutation(data.school, currentUser), { transaction, }); @@ -73,6 +72,7 @@ module.exports = class SubjectsDBApi { importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, + schoolId: resolveSchoolIdForMutation(item.school, currentUser), createdAt: new Date(Date.now() + index * 1000), })); @@ -88,9 +88,8 @@ module.exports = class SubjectsDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; - const subjects = await db.subjects.findByPk(id, {}, {transaction}); + assertRecordInCurrentSchool(subjects, currentUser, 'schoolId'); @@ -112,7 +111,7 @@ module.exports = class SubjectsDBApi { if (data.school !== undefined) { await subjects.setSchool( - data.school, + resolveSchoolIdForMutation(data.school, currentUser), { transaction } ); @@ -140,6 +139,8 @@ module.exports = class SubjectsDBApi { transaction, }); + subjects.forEach((record) => assertRecordInCurrentSchool(record, currentUser, 'schoolId')); + await db.sequelize.transaction(async (transaction) => { for (const record of subjects) { await record.update( @@ -161,6 +162,7 @@ module.exports = class SubjectsDBApi { const transaction = (options && options.transaction) || undefined; const subjects = await db.subjects.findByPk(id, options); + assertRecordInCurrentSchool(subjects, currentUser, 'schoolId'); await subjects.update({ deletedBy: currentUser.id @@ -177,6 +179,9 @@ module.exports = class SubjectsDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || null; + + applySchoolScope(where, currentUser?.app_role?.globalAccess, currentUser, 'schoolId'); const subjects = await db.subjects.findOne( { where }, @@ -233,26 +238,11 @@ module.exports = class SubjectsDBApi { let offset = 0; let where = {}; const currentPage = +filter.page; - - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; - - - - if (userSchools) { - if (options?.currentUser?.schoolsId) { - where.schoolsId = options.currentUser.schoolsId; - } - } offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ { @@ -349,11 +339,7 @@ module.exports = class SubjectsDBApi { } - - if (globalAccess) { - delete where.schoolsId; - } - + applySchoolScope(where, globalAccess, user, 'schoolId'); const queryOptions = { where, @@ -384,14 +370,9 @@ module.exports = class SubjectsDBApi { } } - static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { + static async findAllAutocomplete(query, limit, offset, globalAccess, schoolId,) { let where = {}; - - if (!globalAccess && organizationId) { - where.organizationId = organizationId; - } - if (query) { where = { @@ -406,6 +387,8 @@ module.exports = class SubjectsDBApi { }; } + applySchoolScopeById(where, globalAccess, schoolId, 'schoolId'); + const records = await db.subjects.findAll({ attributes: [ 'id', 'nome' ], where, diff --git a/backend/src/db/api/teachers.js b/backend/src/db/api/teachers.js index 80484e8..809953a 100644 --- a/backend/src/db/api/teachers.js +++ b/backend/src/db/api/teachers.js @@ -1,8 +1,7 @@ const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); const Utils = require('../utils'); +const { applySchoolScope, applySchoolScopeById, assertRecordInCurrentSchool, resolveSchoolIdForMutation } = require('./schoolScope'); @@ -54,7 +53,7 @@ module.exports = class TeachersDBApi { ); - await teachers.setSchool( data.school || null, { + await teachers.setSchool(resolveSchoolIdForMutation(data.school, currentUser), { transaction, }); @@ -103,6 +102,7 @@ module.exports = class TeachersDBApi { importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, + schoolId: resolveSchoolIdForMutation(item.school, currentUser), createdAt: new Date(Date.now() + index * 1000), })); @@ -118,9 +118,8 @@ module.exports = class TeachersDBApi { static async update(id, data, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; - const globalAccess = currentUser.app_role?.globalAccess; - const teachers = await db.teachers.findByPk(id, {}, {transaction}); + assertRecordInCurrentSchool(teachers, currentUser, 'schoolId'); @@ -151,7 +150,7 @@ module.exports = class TeachersDBApi { if (data.school !== undefined) { await teachers.setSchool( - data.school, + resolveSchoolIdForMutation(data.school, currentUser), { transaction } ); @@ -179,6 +178,8 @@ module.exports = class TeachersDBApi { transaction, }); + teachers.forEach((record) => assertRecordInCurrentSchool(record, currentUser, 'schoolId')); + await db.sequelize.transaction(async (transaction) => { for (const record of teachers) { await record.update( @@ -200,6 +201,7 @@ module.exports = class TeachersDBApi { const transaction = (options && options.transaction) || undefined; const teachers = await db.teachers.findByPk(id, options); + assertRecordInCurrentSchool(teachers, currentUser, 'schoolId'); await teachers.update({ deletedBy: currentUser.id @@ -216,6 +218,9 @@ module.exports = class TeachersDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || null; + + applySchoolScope(where, currentUser?.app_role?.globalAccess, currentUser, 'schoolId'); const teachers = await db.teachers.findOne( { where }, @@ -272,26 +277,11 @@ module.exports = class TeachersDBApi { let offset = 0; let where = {}; const currentPage = +filter.page; - - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; - - - - if (userSchools) { - if (options?.currentUser?.schoolsId) { - where.schoolsId = options.currentUser.schoolsId; - } - } offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ { @@ -421,11 +411,7 @@ module.exports = class TeachersDBApi { } - - if (globalAccess) { - delete where.schoolsId; - } - + applySchoolScope(where, globalAccess, user, 'schoolId'); const queryOptions = { where, @@ -456,14 +442,9 @@ module.exports = class TeachersDBApi { } } - static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { + static async findAllAutocomplete(query, limit, offset, globalAccess, schoolId,) { let where = {}; - - if (!globalAccess && organizationId) { - where.organizationId = organizationId; - } - if (query) { where = { @@ -478,6 +459,8 @@ module.exports = class TeachersDBApi { }; } + applySchoolScopeById(where, globalAccess, schoolId, 'schoolId'); + const records = await db.teachers.findAll({ attributes: [ 'id', 'nome' ], where, diff --git a/backend/src/db/api/users.js b/backend/src/db/api/users.js index ceaf393..c6a6012 100644 --- a/backend/src/db/api/users.js +++ b/backend/src/db/api/users.js @@ -3,6 +3,8 @@ const db = require('../models'); const FileDBApi = require('./file'); const crypto = require('crypto'); const Utils = require('../utils'); +const ForbiddenError = require('../../services/notifications/errors/forbidden'); +const { applySchoolScope, applySchoolScopeById, assertRecordInCurrentSchool, resolveSchoolIdForMutation } = require('./schoolScope'); const bcrypt = require('bcrypt'); const config = require('../../config'); @@ -12,12 +14,71 @@ const config = require('../../config'); const Sequelize = db.Sequelize; const Op = Sequelize.Op; + +async function assertAssignableRole(roleId, currentUser, transaction) { + if (!roleId || currentUser?.app_role?.globalAccess) { + return; + } + + const role = await db.roles.findByPk(roleId, { transaction }); + if (!role || role.globalAccess) { + throw new ForbiddenError('auth.forbidden'); + } +} + + +function assertSafeSelfUpdate(id, data, currentUser) { + if ( + !currentUser?.id || + String(currentUser.id) !== String(id) || + currentUser?.app_role?.globalAccess + ) { + return; + } + + const restrictedFields = [ + 'app_role', + 'custom_permissions', + 'schools', + 'disabled', + 'email', + 'emailVerified', + 'emailVerificationToken', + 'emailVerificationTokenExpiresAt', + 'passwordResetToken', + 'passwordResetTokenExpiresAt', + 'provider', + ]; + + const hasRestrictedField = restrictedFields.some((field) => ( + Object.prototype.hasOwnProperty.call(data || {}, field) && data[field] !== undefined + )); + + if (hasRestrictedField) { + throw new ForbiddenError('auth.forbidden'); + } +} + +function assertCanAssignCustomPermissions(customPermissions, currentUser) { + if (currentUser?.app_role?.globalAccess || customPermissions === undefined || customPermissions === null) { + return; + } + + if (Array.isArray(customPermissions) && customPermissions.length === 0) { + return; + } + + throw new ForbiddenError('auth.forbidden'); +} + module.exports = class UsersDBApi { static async create(data,globalAccess, options) { const currentUser = (options && options.currentUser) || { id: null }; const transaction = (options && options.transaction) || undefined; + await assertAssignableRole(data.data.app_role, currentUser, transaction); + assertCanAssignCustomPermissions(data.data.custom_permissions, currentUser); const users = await db.users.create( { @@ -112,7 +173,7 @@ module.exports = class UsersDBApi { - await users.setSchools( data.data.schools || null, { + await users.setSchools(resolveSchoolIdForMutation(data.data.schools, currentUser), { transaction, }); @@ -215,6 +276,7 @@ module.exports = class UsersDBApi { importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, + schoolsId: resolveSchoolIdForMutation(item.schools, currentUser), createdAt: new Date(Date.now() + index * 1000), })); @@ -242,10 +304,20 @@ module.exports = class UsersDBApi { static async update(id, data, globalAccess, options) { const currentUser = (options && options.currentUser) || {id: null}; const transaction = (options && options.transaction) || undefined; + assertSafeSelfUpdate(id, data, currentUser); + const appRoleProvided = Object.prototype.hasOwnProperty.call(data || {}, 'app_role'); + const customPermissionsProvided = Object.prototype.hasOwnProperty.call(data || {}, 'custom_permissions'); const users = await db.users.findByPk(id, {}, {transaction}); + assertRecordInCurrentSchool(users, currentUser, 'schoolsId'); + if (appRoleProvided) { + await assertAssignableRole(data.app_role, currentUser, transaction); + } + if (customPermissionsProvided) { + assertCanAssignCustomPermissions(data.custom_permissions, currentUser); + } @@ -324,7 +396,7 @@ module.exports = class UsersDBApi { if (data.schools !== undefined) { await users.setSchools( - data.schools, + resolveSchoolIdForMutation(data.schools, currentUser), { transaction } ); @@ -339,15 +411,17 @@ module.exports = class UsersDBApi { - await FileDBApi.replaceRelationFiles( - { - belongsTo: db.users.getTableName(), - belongsToColumn: 'avatar', - belongsToId: users.id, - }, - data.avatar, - options, - ); + if (data.avatar !== undefined) { + await FileDBApi.replaceRelationFiles( + { + belongsTo: db.users.getTableName(), + belongsToColumn: 'avatar', + belongsToId: users.id, + }, + data.avatar, + options, + ); + } return users; @@ -366,6 +440,8 @@ module.exports = class UsersDBApi { transaction, }); + users.forEach((record) => assertRecordInCurrentSchool(record, currentUser, 'schoolsId')); + await db.sequelize.transaction(async (transaction) => { for (const record of users) { await record.update( @@ -387,6 +463,7 @@ module.exports = class UsersDBApi { const transaction = (options && options.transaction) || undefined; const users = await db.users.findByPk(id, options); + assertRecordInCurrentSchool(users, currentUser, 'schoolsId'); await users.update({ deletedBy: currentUser.id @@ -403,6 +480,9 @@ module.exports = class UsersDBApi { static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; + const currentUser = (options && options.currentUser) || null; + + applySchoolScope(where, currentUser?.app_role?.globalAccess, currentUser, 'schoolsId'); const users = await db.users.findOne( { where }, @@ -476,26 +556,11 @@ module.exports = class UsersDBApi { let offset = 0; let where = {}; const currentPage = +filter.page; - - const user = (options && options.currentUser) || null; - const userSchools = (user && user.schools?.id) || null; - - - - if (userSchools) { - if (options?.currentUser?.schoolsId) { - where.schoolsId = options.currentUser.schoolsId; - } - } offset = currentPage * limit; - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - let include = [ { @@ -777,11 +842,7 @@ module.exports = class UsersDBApi { } - - if (globalAccess) { - delete where.schoolsId; - } - + applySchoolScope(where, globalAccess, user, 'schoolsId'); const queryOptions = { where, @@ -812,14 +873,9 @@ module.exports = class UsersDBApi { } } - static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) { + static async findAllAutocomplete(query, limit, offset, globalAccess, schoolId,) { let where = {}; - - if (!globalAccess && organizationId) { - where.organizationId = organizationId; - } - if (query) { where = { @@ -834,6 +890,8 @@ module.exports = class UsersDBApi { }; } + applySchoolScopeById(where, globalAccess, schoolId, 'schoolsId'); + const records = await db.users.findAll({ attributes: [ 'id', 'firstName' ], where, @@ -858,7 +916,7 @@ module.exports = class UsersDBApi { authenticationUid: data.authenticationUid, password: data.password, - organizationId: data.organizationId, + schoolsId: data.organizationId, }, { transaction }, diff --git a/backend/src/db/migrations/1782175325418.js b/backend/src/db/migrations/1782175325418.js index 8ab360e..814884d 100644 --- a/backend/src/db/migrations/1782175325418.js +++ b/backend/src/db/migrations/1782175325418.js @@ -2777,7 +2777,7 @@ module.exports = { * @param {Sequelize} Sequelize * @returns {Promise} */ - async down(queryInterface, Sequelize) { + async down(queryInterface) { /** * @type {Transaction} */ diff --git a/backend/src/db/models/assessments.js b/backend/src/db/models/assessments.js index 1d58d90..ded73de 100644 --- a/backend/src/db/models/assessments.js +++ b/backend/src/db/models/assessments.js @@ -1,8 +1,3 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); module.exports = function(sequelize, DataTypes) { const assessments = sequelize.define( diff --git a/backend/src/db/models/attendance.js b/backend/src/db/models/attendance.js index 9cbceb2..6db80ef 100644 --- a/backend/src/db/models/attendance.js +++ b/backend/src/db/models/attendance.js @@ -1,8 +1,3 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); module.exports = function(sequelize, DataTypes) { const attendance = sequelize.define( diff --git a/backend/src/db/models/book_loans.js b/backend/src/db/models/book_loans.js index d8cfb24..c478841 100644 --- a/backend/src/db/models/book_loans.js +++ b/backend/src/db/models/book_loans.js @@ -1,8 +1,3 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); module.exports = function(sequelize, DataTypes) { const book_loans = sequelize.define( diff --git a/backend/src/db/models/books.js b/backend/src/db/models/books.js index eda08fd..b1a0fb4 100644 --- a/backend/src/db/models/books.js +++ b/backend/src/db/models/books.js @@ -1,8 +1,3 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); module.exports = function(sequelize, DataTypes) { const books = sequelize.define( diff --git a/backend/src/db/models/classes.js b/backend/src/db/models/classes.js index 5bb7f7b..0e6c52f 100644 --- a/backend/src/db/models/classes.js +++ b/backend/src/db/models/classes.js @@ -1,8 +1,3 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); module.exports = function(sequelize, DataTypes) { const classes = sequelize.define( diff --git a/backend/src/db/models/courses.js b/backend/src/db/models/courses.js index d038d78..7080f02 100644 --- a/backend/src/db/models/courses.js +++ b/backend/src/db/models/courses.js @@ -1,8 +1,3 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); module.exports = function(sequelize, DataTypes) { const courses = sequelize.define( diff --git a/backend/src/db/models/employees.js b/backend/src/db/models/employees.js index 75ce475..9487bbd 100644 --- a/backend/src/db/models/employees.js +++ b/backend/src/db/models/employees.js @@ -1,8 +1,3 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); module.exports = function(sequelize, DataTypes) { const employees = sequelize.define( diff --git a/backend/src/db/models/enrollments.js b/backend/src/db/models/enrollments.js index 80df3ae..5810b1f 100644 --- a/backend/src/db/models/enrollments.js +++ b/backend/src/db/models/enrollments.js @@ -1,8 +1,3 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); module.exports = function(sequelize, DataTypes) { const enrollments = sequelize.define( diff --git a/backend/src/db/models/grades.js b/backend/src/db/models/grades.js index 6c14195..25e75b8 100644 --- a/backend/src/db/models/grades.js +++ b/backend/src/db/models/grades.js @@ -1,8 +1,3 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); module.exports = function(sequelize, DataTypes) { const grades = sequelize.define( diff --git a/backend/src/db/models/guardians.js b/backend/src/db/models/guardians.js index be40072..1d508e8 100644 --- a/backend/src/db/models/guardians.js +++ b/backend/src/db/models/guardians.js @@ -1,8 +1,3 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); module.exports = function(sequelize, DataTypes) { const guardians = sequelize.define( diff --git a/backend/src/db/models/invoices.js b/backend/src/db/models/invoices.js index 8119ce8..77ed48e 100644 --- a/backend/src/db/models/invoices.js +++ b/backend/src/db/models/invoices.js @@ -1,8 +1,3 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); module.exports = function(sequelize, DataTypes) { const invoices = sequelize.define( diff --git a/backend/src/db/models/payments.js b/backend/src/db/models/payments.js index 5d6a50d..28fff06 100644 --- a/backend/src/db/models/payments.js +++ b/backend/src/db/models/payments.js @@ -1,8 +1,3 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); module.exports = function(sequelize, DataTypes) { const payments = sequelize.define( diff --git a/backend/src/db/models/permissions.js b/backend/src/db/models/permissions.js index ac23908..016ebed 100644 --- a/backend/src/db/models/permissions.js +++ b/backend/src/db/models/permissions.js @@ -1,8 +1,3 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); module.exports = function(sequelize, DataTypes) { const permissions = sequelize.define( diff --git a/backend/src/db/models/products.js b/backend/src/db/models/products.js index 639f03d..fa1d0a9 100644 --- a/backend/src/db/models/products.js +++ b/backend/src/db/models/products.js @@ -1,8 +1,3 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); module.exports = function(sequelize, DataTypes) { const products = sequelize.define( diff --git a/backend/src/db/models/roles.js b/backend/src/db/models/roles.js index 148a533..3f0e9a4 100644 --- a/backend/src/db/models/roles.js +++ b/backend/src/db/models/roles.js @@ -1,8 +1,3 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); module.exports = function(sequelize, DataTypes) { const roles = sequelize.define( diff --git a/backend/src/db/models/student_guardians.js b/backend/src/db/models/student_guardians.js index 398790c..bea615e 100644 --- a/backend/src/db/models/student_guardians.js +++ b/backend/src/db/models/student_guardians.js @@ -1,8 +1,3 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); module.exports = function(sequelize, DataTypes) { const student_guardians = sequelize.define( diff --git a/backend/src/db/models/students.js b/backend/src/db/models/students.js index 32d54d8..2518d3f 100644 --- a/backend/src/db/models/students.js +++ b/backend/src/db/models/students.js @@ -1,8 +1,3 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); module.exports = function(sequelize, DataTypes) { const students = sequelize.define( diff --git a/backend/src/db/models/subjects.js b/backend/src/db/models/subjects.js index 62ebf2f..c25cfa5 100644 --- a/backend/src/db/models/subjects.js +++ b/backend/src/db/models/subjects.js @@ -1,8 +1,3 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); module.exports = function(sequelize, DataTypes) { const subjects = sequelize.define( diff --git a/backend/src/db/models/teachers.js b/backend/src/db/models/teachers.js index d76eb5e..6d9105b 100644 --- a/backend/src/db/models/teachers.js +++ b/backend/src/db/models/teachers.js @@ -1,8 +1,3 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); module.exports = function(sequelize, DataTypes) { const teachers = sequelize.define( diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.js index 3768315..60b8588 100644 --- a/backend/src/db/models/users.js +++ b/backend/src/db/models/users.js @@ -2,7 +2,6 @@ const config = require('../../config'); const providers = config.providers; const crypto = require('crypto'); const bcrypt = require('bcrypt'); -const moment = require('moment'); module.exports = function(sequelize, DataTypes) { const users = sequelize.define( @@ -206,13 +205,13 @@ provider: { }; - users.beforeCreate((users, options) => { - users = trimStringFields(users); + users.beforeCreate((user) => { + trimStringFields(user); - if (users.provider !== providers.LOCAL && Object.values(providers).indexOf(users.provider) > -1) { - users.emailVerified = true; + if (user.provider !== providers.LOCAL && Object.values(providers).indexOf(user.provider) > -1) { + user.emailVerified = true; - if (!users.password) { + if (!user.password) { const password = crypto .randomBytes(20) .toString('hex'); @@ -222,13 +221,13 @@ provider: { config.bcrypt.saltRounds, ); - users.password = hashedPassword + user.password = hashedPassword } } }); - users.beforeUpdate((users, options) => { - users = trimStringFields(users); + users.beforeUpdate((user) => { + trimStringFields(user); }); diff --git a/backend/src/db/seeders/20200430130759-admin-user.js b/backend/src/db/seeders/20200430130759-admin-user.js index 15022fe..67d7289 100644 --- a/backend/src/db/seeders/20200430130759-admin-user.js +++ b/backend/src/db/seeders/20200430130759-admin-user.js @@ -10,7 +10,7 @@ const ids = [ ] module.exports = { - up: async (queryInterface, Sequelize) => { + up: async (queryInterface) => { let admin_hash = bcrypt.hashSync(config.admin_pass, config.bcrypt.saltRounds); let user_hash = bcrypt.hashSync(config.user_pass, config.bcrypt.saltRounds); diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index 781da8a..ae9380c 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -63,7 +63,7 @@ module.exports = { } const entities = [ - "users","roles","permissions","schools","students","guardians","student_guardians","teachers","courses","grades","classes","subjects","enrollments","assessments","attendance","invoices","payments","employees","products","books","book_loans",, + "users","roles","permissions","schools","students","guardians","student_guardians","teachers","courses","grades","classes","subjects","enrollments","assessments","attendance","invoices","payments","employees","products","books","book_loans", ]; await queryInterface.bulkInsert("permissions", entities.flatMap(createPermissions)); await queryInterface.bulkInsert("permissions", [{ id: getId(`READ_API_DOCS`), createdAt, updatedAt, name: `READ_API_DOCS` }]); diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index d91d4f8..9da4c5d 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -4450,7 +4450,7 @@ const BookLoansData = [ module.exports = { - up: async (queryInterface, Sequelize) => { + up: async () => { @@ -4954,7 +4954,7 @@ module.exports = { }, - down: async (queryInterface, Sequelize) => { + down: async (queryInterface) => { diff --git a/backend/src/helpers.js b/backend/src/helpers.js index c38440d..c0b0688 100644 --- a/backend/src/helpers.js +++ b/backend/src/helpers.js @@ -8,7 +8,7 @@ module.exports = class Helpers { }; } - static commonErrorHandler(error, req, res, next) { + static commonErrorHandler(error, req, res) { if ([400, 403, 404].includes(error.code)) { return res.status(error.code).send(error.message); } @@ -19,5 +19,5 @@ module.exports = class Helpers { static jwtSign(data) { return jwt.sign(data, config.secret_key, {expiresIn: '6h'}); - }; + } }; diff --git a/backend/src/index.js b/backend/src/index.js index dfad902..cdafc02 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -6,7 +6,6 @@ const passport = require('passport'); const path = require('path'); const fs = require('fs'); const bodyParser = require('body-parser'); -const db = require('./db/models'); const config = require('./config'); const swaggerUI = require('swagger-ui-express'); const swaggerJsDoc = require('swagger-jsdoc'); diff --git a/backend/src/middlewares/check-permissions.js b/backend/src/middlewares/check-permissions.js index 77740c7..af03254 100644 --- a/backend/src/middlewares/check-permissions.js +++ b/backend/src/middlewares/check-permissions.js @@ -42,9 +42,12 @@ function checkPermissions(permission) { return async (req, res, next) => { const { currentUser } = req; - // 1. Check self-access bypass (only if the user is authenticated) - if (currentUser && (currentUser.id === req.params.id || currentUser.id === req.body.id)) { - return next(); // User has access to their own resource + // 1. Check self-access bypass for reading the authenticated user's own record only. + const isSelfAccess = currentUser && ( + currentUser.id === req.params.id || currentUser.id === req.body?.id + ); + if (isSelfAccess && permission === 'READ_USERS') { + return next(); } // 2. Check Custom Permissions (only if the user is authenticated) @@ -135,9 +138,10 @@ const METHOD_MAP = { */ function checkCrudPermissions(name) { return (req, res, next) => { - // Dynamically determine the permission name (e.g., 'READ_USERS') - const permissionName = `${METHOD_MAP[req.method]}_${name.toUpperCase()}`; - // Call the checkPermissions middleware with the determined permission + const methodAction = req.path.replace(/\/$/, '') === '/deleteByIds' + ? 'DELETE' + : METHOD_MAP[req.method]; + const permissionName = `${methodAction}_${name.toUpperCase()}`; checkPermissions(permissionName)(req, res, next); }; } diff --git a/backend/src/routes/assessments.js b/backend/src/routes/assessments.js index ca054a4..6f536e8 100644 --- a/backend/src/routes/assessments.js +++ b/backend/src/routes/assessments.js @@ -3,10 +3,9 @@ const express = require('express'); const AssessmentsService = require('../services/assessments'); const AssessmentsDBApi = require('../db/api/assessments'); +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); const wrapAsync = require('../helpers').wrapAsync; -const config = require('../config'); - const router = express.Router(); @@ -309,6 +308,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); @@ -384,14 +384,14 @@ router.get('/autocomplete', async (req, res) => { const globalAccess = req.currentUser.app_role.globalAccess; - const organizationId = req.currentUser.organization?.id + const schoolId = getCurrentUserSchoolId(req.currentUser); const payload = await AssessmentsDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - globalAccess, organizationId, + globalAccess, schoolId, ); res.status(200).send(payload); @@ -432,6 +432,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await AssessmentsDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/attendance.js b/backend/src/routes/attendance.js index ca3dbf6..ce02dd6 100644 --- a/backend/src/routes/attendance.js +++ b/backend/src/routes/attendance.js @@ -3,10 +3,9 @@ const express = require('express'); const AttendanceService = require('../services/attendance'); const AttendanceDBApi = require('../db/api/attendance'); +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); const wrapAsync = require('../helpers').wrapAsync; -const config = require('../config'); - const router = express.Router(); @@ -305,6 +304,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); @@ -380,14 +380,14 @@ router.get('/autocomplete', async (req, res) => { const globalAccess = req.currentUser.app_role.globalAccess; - const organizationId = req.currentUser.organization?.id + const schoolId = getCurrentUserSchoolId(req.currentUser); const payload = await AttendanceDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - globalAccess, organizationId, + globalAccess, schoolId, ); res.status(200).send(payload); @@ -428,6 +428,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await AttendanceDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/book_loans.js b/backend/src/routes/book_loans.js index 05deba9..e5dc19e 100644 --- a/backend/src/routes/book_loans.js +++ b/backend/src/routes/book_loans.js @@ -3,10 +3,9 @@ const express = require('express'); const Book_loansService = require('../services/book_loans'); const Book_loansDBApi = require('../db/api/book_loans'); +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); const wrapAsync = require('../helpers').wrapAsync; -const config = require('../config'); - const router = express.Router(); @@ -306,6 +305,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); @@ -381,14 +381,14 @@ router.get('/autocomplete', async (req, res) => { const globalAccess = req.currentUser.app_role.globalAccess; - const organizationId = req.currentUser.organization?.id + const schoolId = getCurrentUserSchoolId(req.currentUser); const payload = await Book_loansDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - globalAccess, organizationId, + globalAccess, schoolId, ); res.status(200).send(payload); @@ -429,6 +429,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await Book_loansDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/books.js b/backend/src/routes/books.js index 963742d..69fb459 100644 --- a/backend/src/routes/books.js +++ b/backend/src/routes/books.js @@ -3,10 +3,9 @@ const express = require('express'); const BooksService = require('../services/books'); const BooksDBApi = require('../db/api/books'); +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); const wrapAsync = require('../helpers').wrapAsync; -const config = require('../config'); - const router = express.Router(); @@ -318,6 +317,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); @@ -393,14 +393,14 @@ router.get('/autocomplete', async (req, res) => { const globalAccess = req.currentUser.app_role.globalAccess; - const organizationId = req.currentUser.organization?.id + const schoolId = getCurrentUserSchoolId(req.currentUser); const payload = await BooksDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - globalAccess, organizationId, + globalAccess, schoolId, ); res.status(200).send(payload); @@ -441,6 +441,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await BooksDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/classes.js b/backend/src/routes/classes.js index 55cc193..1646e73 100644 --- a/backend/src/routes/classes.js +++ b/backend/src/routes/classes.js @@ -3,10 +3,9 @@ const express = require('express'); const ClassesService = require('../services/classes'); const ClassesDBApi = require('../db/api/classes'); +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); const wrapAsync = require('../helpers').wrapAsync; -const config = require('../config'); - const router = express.Router(); @@ -313,6 +312,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); @@ -388,14 +388,14 @@ router.get('/autocomplete', async (req, res) => { const globalAccess = req.currentUser.app_role.globalAccess; - const organizationId = req.currentUser.organization?.id + const schoolId = getCurrentUserSchoolId(req.currentUser); const payload = await ClassesDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - globalAccess, organizationId, + globalAccess, schoolId, ); res.status(200).send(payload); @@ -436,6 +436,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await ClassesDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/courses.js b/backend/src/routes/courses.js index 614deeb..d349cd8 100644 --- a/backend/src/routes/courses.js +++ b/backend/src/routes/courses.js @@ -3,10 +3,9 @@ const express = require('express'); const CoursesService = require('../services/courses'); const CoursesDBApi = require('../db/api/courses'); +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); const wrapAsync = require('../helpers').wrapAsync; -const config = require('../config'); - const router = express.Router(); @@ -309,6 +308,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); @@ -384,14 +384,14 @@ router.get('/autocomplete', async (req, res) => { const globalAccess = req.currentUser.app_role.globalAccess; - const organizationId = req.currentUser.organization?.id + const schoolId = getCurrentUserSchoolId(req.currentUser); const payload = await CoursesDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - globalAccess, organizationId, + globalAccess, schoolId, ); res.status(200).send(payload); @@ -432,6 +432,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await CoursesDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/employees.js b/backend/src/routes/employees.js index 1376fe5..2776d12 100644 --- a/backend/src/routes/employees.js +++ b/backend/src/routes/employees.js @@ -3,10 +3,9 @@ const express = require('express'); const EmployeesService = require('../services/employees'); const EmployeesDBApi = require('../db/api/employees'); +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); const wrapAsync = require('../helpers').wrapAsync; -const config = require('../config'); - const router = express.Router(); @@ -315,6 +314,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); @@ -390,14 +390,14 @@ router.get('/autocomplete', async (req, res) => { const globalAccess = req.currentUser.app_role.globalAccess; - const organizationId = req.currentUser.organization?.id + const schoolId = getCurrentUserSchoolId(req.currentUser); const payload = await EmployeesDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - globalAccess, organizationId, + globalAccess, schoolId, ); res.status(200).send(payload); @@ -438,6 +438,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await EmployeesDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/enrollments.js b/backend/src/routes/enrollments.js index 398ee1b..cf5de9a 100644 --- a/backend/src/routes/enrollments.js +++ b/backend/src/routes/enrollments.js @@ -3,10 +3,9 @@ const express = require('express'); const EnrollmentsService = require('../services/enrollments'); const EnrollmentsDBApi = require('../db/api/enrollments'); +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); const wrapAsync = require('../helpers').wrapAsync; -const config = require('../config'); - const router = express.Router(); @@ -306,6 +305,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); @@ -381,14 +381,14 @@ router.get('/autocomplete', async (req, res) => { const globalAccess = req.currentUser.app_role.globalAccess; - const organizationId = req.currentUser.organization?.id + const schoolId = getCurrentUserSchoolId(req.currentUser); const payload = await EnrollmentsDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - globalAccess, organizationId, + globalAccess, schoolId, ); res.status(200).send(payload); @@ -429,6 +429,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await EnrollmentsDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/file.js b/backend/src/routes/file.js index ddd2bc0..cbb3c03 100644 --- a/backend/src/routes/file.js +++ b/backend/src/routes/file.js @@ -1,21 +1,24 @@ const express = require('express'); -const config = require('../config'); -const path = require('path'); const passport = require('passport'); const services = require('../services/file'); +const wrapAsync = require('../helpers').wrapAsync; const router = express.Router(); -router.get('/download', (req, res) => { +router.get('/download', wrapAsync(async (req, res) => { if (process.env.NODE_ENV == "production" || process.env.NEXT_PUBLIC_BACK_API) { - services.downloadGCloud(req, res); + await services.downloadGCloud(req, res); } else { - services.downloadLocal(req, res); + await services.downloadLocal(req, res); } -}); +})); router.post('/upload/:table/:field', passport.authenticate('jwt', {session: false}), (req, res) => { - const fileName = `${req.params.table}/${req.params.field}`; + const fileName = services.normalizeFolder(`${req.params.table}/${req.params.field}`); + + if (!fileName) { + return res.status(400).send({ message: 'Invalid upload path.' }); + } if (process.env.NODE_ENV == "production" || process.env.NEXT_PUBLIC_BACK_API) { services.uploadGCloud(fileName, req, res); @@ -29,4 +32,7 @@ router.post('/upload/:table/:field', passport.authenticate('jwt', {session: fals } }); + +router.use('/', require('../helpers').commonErrorHandler); + module.exports = router; diff --git a/backend/src/routes/grades.js b/backend/src/routes/grades.js index 9616546..353044f 100644 --- a/backend/src/routes/grades.js +++ b/backend/src/routes/grades.js @@ -3,10 +3,9 @@ const express = require('express'); const GradesService = require('../services/grades'); const GradesDBApi = require('../db/api/grades'); +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); const wrapAsync = require('../helpers').wrapAsync; -const config = require('../config'); - const router = express.Router(); @@ -309,6 +308,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); @@ -384,14 +384,14 @@ router.get('/autocomplete', async (req, res) => { const globalAccess = req.currentUser.app_role.globalAccess; - const organizationId = req.currentUser.organization?.id + const schoolId = getCurrentUserSchoolId(req.currentUser); const payload = await GradesDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - globalAccess, organizationId, + globalAccess, schoolId, ); res.status(200).send(payload); @@ -432,6 +432,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await GradesDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/guardians.js b/backend/src/routes/guardians.js index 030aec8..14162cb 100644 --- a/backend/src/routes/guardians.js +++ b/backend/src/routes/guardians.js @@ -3,10 +3,9 @@ const express = require('express'); const GuardiansService = require('../services/guardians'); const GuardiansDBApi = require('../db/api/guardians'); +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); const wrapAsync = require('../helpers').wrapAsync; -const config = require('../config'); - const router = express.Router(); @@ -312,6 +311,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); @@ -387,14 +387,14 @@ router.get('/autocomplete', async (req, res) => { const globalAccess = req.currentUser.app_role.globalAccess; - const organizationId = req.currentUser.organization?.id + const schoolId = getCurrentUserSchoolId(req.currentUser); const payload = await GuardiansDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - globalAccess, organizationId, + globalAccess, schoolId, ); res.status(200).send(payload); @@ -435,6 +435,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await GuardiansDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/invoices.js b/backend/src/routes/invoices.js index 9e9300e..7d489d8 100644 --- a/backend/src/routes/invoices.js +++ b/backend/src/routes/invoices.js @@ -3,10 +3,9 @@ const express = require('express'); const InvoicesService = require('../services/invoices'); const InvoicesDBApi = require('../db/api/invoices'); +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); const wrapAsync = require('../helpers').wrapAsync; -const config = require('../config'); - const router = express.Router(); @@ -312,6 +311,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); @@ -387,14 +387,14 @@ router.get('/autocomplete', async (req, res) => { const globalAccess = req.currentUser.app_role.globalAccess; - const organizationId = req.currentUser.organization?.id + const schoolId = getCurrentUserSchoolId(req.currentUser); const payload = await InvoicesDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - globalAccess, organizationId, + globalAccess, schoolId, ); res.status(200).send(payload); @@ -435,6 +435,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await InvoicesDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/openai.js b/backend/src/routes/openai.js index 2d47d9f..44580c4 100644 --- a/backend/src/routes/openai.js +++ b/backend/src/routes/openai.js @@ -5,6 +5,109 @@ const router = express.Router(); const sjs = require('sequelize-json-schema'); const { getWidget, askGpt } = require('../services/openai'); const { LocalAIApi } = require('../ai/LocalAIApi'); +const ForbiddenError = require('../services/notifications/errors/forbidden'); +const { checkPermissions } = require('../middlewares/check-permissions'); + + +const WIDGET_CUSTOMIZATION_KEY = 'widgets'; +const UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + +function badRequest(message) { + const error = new Error(message); + error.code = 400; + return error; +} + +function normalizeWidgetKey(key) { + if (key !== WIDGET_CUSTOMIZATION_KEY) { + throw badRequest('Invalid role customization key.'); + } + + return key; +} + +function normalizeUuid(value, label) { + if (typeof value !== 'string' || !UUID_REGEX.test(value)) { + throw badRequest(`${label} must be a valid UUID.`); + } + + return value; +} + +function normalizeText(value, label, maxLength) { + if (typeof value !== 'string' || !value.trim()) { + throw badRequest(`${label} is required.`); + } + + const normalized = value.trim(); + if (normalized.length > maxLength) { + throw badRequest(`${label} is too long. Maximum length is ${maxLength} characters.`); + } + + return normalized; +} + +function normalizeNumberOption(value, defaultValue, minValue, maxValue) { + const parsed = Number(value); + if (!Number.isFinite(parsed)) { + return defaultValue; + } + + return Math.min(Math.max(parsed, minValue), maxValue); +} + +function normalizeAiProxyOptions(options = {}) { + return { + poll_interval: normalizeNumberOption(options.poll_interval, 5, 1, 30), + poll_timeout: normalizeNumberOption(options.poll_timeout, 300, 5, 300), + timeout: normalizeNumberOption(options.timeout, 30, 5, 60), + }; +} + +function assertGlobalRoleAccess(currentUser) { + if (!currentUser?.app_role?.globalAccess) { + throw new ForbiddenError('auth.forbidden'); + } +} + +async function hasRolePermission(currentUser, permission) { + if (!currentUser) { + return false; + } + + const customPermissions = Array.isArray(currentUser.custom_permissions) + ? currentUser.custom_permissions + : []; + + if (customPermissions.find((cp) => cp.name === permission)) { + return true; + } + + if (!currentUser.app_role) { + return false; + } + + const permissions = typeof currentUser.app_role.getPermissions === 'function' + ? await currentUser.app_role.getPermissions() + : currentUser.app_role.permissions || []; + + return !!permissions.find((p) => p.name === permission); +} + +async function assertCanReadRoleWidgets(roleId, currentUser) { + if (roleId === currentUser?.app_role?.id) { + return; + } + + if (!(await hasRolePermission(currentUser, 'READ_ROLES'))) { + throw new ForbiddenError('auth.forbidden'); + } +} + +async function canGenerateRoleWidgets(currentUser) { + return !!currentUser?.app_role?.globalAccess + && await hasRolePermission(currentUser, 'UPDATE_ROLES'); +} const loadRolesModules = () => { try { @@ -71,12 +174,17 @@ const loadRolesModules = () => { router.delete( '/roles-info/:infoId', + checkPermissions('UPDATE_ROLES'), wrapAsync(async (req, res) => { + assertGlobalRoleAccess(req.currentUser); const { RolesService } = loadRolesModules(); + const key = normalizeWidgetKey(req.query.key); + const infoId = normalizeUuid(req.query.infoId || req.params.infoId, 'Widget ID'); + const roleId = normalizeUuid(req.query.roleId, 'Role ID'); const role = await RolesService.removeRoleInfoById( - req.query.infoId, - req.query.roleId, - req.query.key, + infoId, + roleId, + key, req.currentUser, ); @@ -131,33 +239,46 @@ router.get( '/info-by-key', wrapAsync(async (req, res) => { const { RolesService, RolesDBApi } = loadRolesModules(); - const roleId = req.query.roleId; - const key = req.query.key; const currentUser = req.currentUser; + const key = normalizeWidgetKey(req.query.key); + const roleId = normalizeUuid(req.query.roleId || currentUser?.app_role?.id, 'Role ID'); + + await assertCanReadRoleWidgets(roleId, currentUser); + + const role = await RolesDBApi.findBy({ id: roleId }); + if (!role) { + return res.status(404).send('Role not found'); + } + let info = await RolesService.getRoleInfoByKey( key, roleId, currentUser, ); - const role = await RolesDBApi.findBy({ id: roleId }); - if (!role?.role_customization) { - await Promise.all(["pie", "bar"].map(async (e) => { - const schema = await sjs.getSequelizeSchema(db.sequelize, {}); + + if (!role.role_customization && await canGenerateRoleWidgets(currentUser)) { + const schema = await sjs.getSequelizeSchema(db.sequelize, {}); + const widgetResults = await Promise.allSettled(['pie', 'bar'].map(async (chartType) => { const payload = { - description: `Create some cool ${e} chart`, + description: `Create some cool ${chartType} chart`, modelDefinition: schema.definitions, }; - const widgetId = await getWidget(payload, currentUser?.id, roleId); - if (widgetId) { + const widgetId = await getWidget(payload); + if (typeof widgetId === 'string') { await RolesService.addRoleInfo( roleId, currentUser?.id, - 'widgets', + WIDGET_CUSTOMIZATION_KEY, widgetId, req.currentUser, ); } - })) + })); + + widgetResults + .filter((result) => result.status === 'rejected') + .forEach((result) => console.error('Default widget creation failed:', result.reason)); + info = await RolesService.getRoleInfoByKey( key, roleId, @@ -170,9 +291,13 @@ router.get( router.post( '/create_widget', + checkPermissions('UPDATE_ROLES'), wrapAsync(async (req, res) => { + assertGlobalRoleAccess(req.currentUser); const { RolesService } = loadRolesModules(); - const { description, userId, roleId } = req.body; + const { userId } = req.body || {}; + const description = normalizeText(req.body?.description, 'Description', 1000); + const roleId = normalizeUuid(req.body?.roleId, 'Role ID'); const currentUser = req.currentUser; const schema = await sjs.getSequelizeSchema(db.sequelize, {}); @@ -181,13 +306,13 @@ router.post( modelDefinition: schema.definitions, }; - const widgetId = await getWidget(payload, userId, roleId); + const widgetId = await getWidget(payload); - if (widgetId) { + if (typeof widgetId === 'string') { await RolesService.addRoleInfo( roleId, userId, - 'widgets', + WIDGET_CUSTOMIZATION_KEY, widgetId, currentUser, ); @@ -247,9 +372,10 @@ router.post( '/response', wrapAsync(async (req, res) => { const body = req.body || {}; - const options = body.options || {}; + const options = normalizeAiProxyOptions(body.options || {}); const payload = { ...body }; delete payload.options; + delete payload.project_uuid; const response = await LocalAIApi.createResponse(payload, options); @@ -306,13 +432,7 @@ router.post( router.post( '/ask-gpt', wrapAsync(async (req, res) => { - const { prompt } = req.body; - if (!prompt) { - return res.status(400).send({ - success: false, - error: 'Prompt is required', - }); - } + const prompt = normalizeText(req.body?.prompt, 'Prompt', 4000); const response = await askGpt(prompt); @@ -325,4 +445,6 @@ router.post( ); +router.use('/', require('../helpers').commonErrorHandler); + module.exports = router; diff --git a/backend/src/routes/payments.js b/backend/src/routes/payments.js index c4ff45e..665fee3 100644 --- a/backend/src/routes/payments.js +++ b/backend/src/routes/payments.js @@ -3,10 +3,9 @@ const express = require('express'); const PaymentsService = require('../services/payments'); const PaymentsDBApi = require('../db/api/payments'); +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); const wrapAsync = require('../helpers').wrapAsync; -const config = require('../config'); - const router = express.Router(); @@ -309,6 +308,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); @@ -384,14 +384,14 @@ router.get('/autocomplete', async (req, res) => { const globalAccess = req.currentUser.app_role.globalAccess; - const organizationId = req.currentUser.organization?.id + const schoolId = getCurrentUserSchoolId(req.currentUser); const payload = await PaymentsDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - globalAccess, organizationId, + globalAccess, schoolId, ); res.status(200).send(payload); @@ -432,6 +432,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await PaymentsDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/permissions.js b/backend/src/routes/permissions.js index b569a78..e528b5e 100644 --- a/backend/src/routes/permissions.js +++ b/backend/src/routes/permissions.js @@ -301,6 +301,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); diff --git a/backend/src/routes/products.js b/backend/src/routes/products.js index aab36f9..e671ecf 100644 --- a/backend/src/routes/products.js +++ b/backend/src/routes/products.js @@ -3,10 +3,9 @@ const express = require('express'); const ProductsService = require('../services/products'); const ProductsDBApi = require('../db/api/products'); +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); const wrapAsync = require('../helpers').wrapAsync; -const config = require('../config'); - const router = express.Router(); @@ -315,6 +314,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); @@ -390,14 +390,14 @@ router.get('/autocomplete', async (req, res) => { const globalAccess = req.currentUser.app_role.globalAccess; - const organizationId = req.currentUser.organization?.id + const schoolId = getCurrentUserSchoolId(req.currentUser); const payload = await ProductsDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - globalAccess, organizationId, + globalAccess, schoolId, ); res.status(200).send(payload); @@ -438,6 +438,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await ProductsDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/roles.js b/backend/src/routes/roles.js index 91ceba8..10674c1 100644 --- a/backend/src/routes/roles.js +++ b/backend/src/routes/roles.js @@ -5,8 +5,6 @@ const RolesService = require('../services/roles'); const RolesDBApi = require('../db/api/roles'); const wrapAsync = require('../helpers').wrapAsync; -const config = require('../config'); - const router = express.Router(); @@ -305,6 +303,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); diff --git a/backend/src/routes/schools.js b/backend/src/routes/schools.js index 13f49bd..5285dcd 100644 --- a/backend/src/routes/schools.js +++ b/backend/src/routes/schools.js @@ -3,10 +3,9 @@ const express = require('express'); const SchoolsService = require('../services/schools'); const SchoolsDBApi = require('../db/api/schools'); +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); const wrapAsync = require('../helpers').wrapAsync; -const config = require('../config'); - const router = express.Router(); @@ -314,6 +313,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); @@ -389,14 +389,14 @@ router.get('/autocomplete', async (req, res) => { const globalAccess = req.currentUser.app_role.globalAccess; - const organizationId = req.currentUser.organization?.id + const schoolId = getCurrentUserSchoolId(req.currentUser); const payload = await SchoolsDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - globalAccess, organizationId, + globalAccess, schoolId, ); res.status(200).send(payload); @@ -437,6 +437,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await SchoolsDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/search.js b/backend/src/routes/search.js index 25da9e0..ddf856e 100644 --- a/backend/src/routes/search.js +++ b/backend/src/routes/search.js @@ -1,8 +1,6 @@ const express = require('express'); const SearchService = require('../services/search'); - -const config = require('../config'); - +const wrapAsync = require('../helpers').wrapAsync; const router = express.Router(); @@ -35,22 +33,14 @@ router.use(checkCrudPermissions('search')); * description: Internal server error */ -router.post('/', async (req, res) => { - const { searchQuery , organizationId} = req.body; - - const globalAccess = req.currentUser.app_role.globalAccess; - - if (!searchQuery) { - return res.status(400).json({ error: 'Please enter a search query' }); - } - - try { - const foundMatches = await SearchService.search(searchQuery, req.currentUser , organizationId, globalAccess,); - res.json(foundMatches); - } catch (error) { - console.error('Internal Server Error', error); - res.status(500).json({ error: 'Internal Server Error' }); - } - }); +router.post('/', wrapAsync(async (req, res) => { + const { searchQuery } = req.body || {}; + const globalAccess = !!req.currentUser?.app_role?.globalAccess; -module.exports = router; \ No newline at end of file + const foundMatches = await SearchService.search(searchQuery, req.currentUser, globalAccess); + res.json(foundMatches); +})); + +router.use('/', require('../helpers').commonErrorHandler); + +module.exports = router; diff --git a/backend/src/routes/sql.js b/backend/src/routes/sql.js index b844f07..081218f 100644 --- a/backend/src/routes/sql.js +++ b/backend/src/routes/sql.js @@ -1,6 +1,7 @@ const express = require('express'); -const db = require('../db/models'); const wrapAsync = require('../helpers').wrapAsync; +const ForbiddenError = require('../services/notifications/errors/forbidden'); +const { executeReadOnlySelect } = require('../services/sqlSafety'); const router = express.Router(); @@ -30,32 +31,27 @@ const router = express.Router(); * description: Invalid SQL * 401: * $ref: "#/components/responses/UnauthorizedError" + * 403: + * description: Requires global access * 500: * description: Internal server error */ router.post( '/', wrapAsync(async (req, res) => { - const { sql } = req.body; - if (typeof sql !== 'string' || !sql.trim()) { - return res.status(400).json({ error: 'SQL is required' }); + if (!req.currentUser?.app_role?.globalAccess) { + throw new ForbiddenError('auth.forbidden'); } - const normalized = sql.trim().replace(/;+\s*$/, ''); - if (!/^select\b/i.test(normalized)) { - return res.status(400).json({ error: 'Only SELECT statements are allowed' }); - } - - if (normalized.includes(';')) { - return res.status(400).json({ error: 'Only a single SELECT statement is allowed' }); - } - - const rows = await db.sequelize.query(normalized, { - type: db.Sequelize.QueryTypes.SELECT, + const body = req.body || {}; + const rows = await executeReadOnlySelect(body.sql, { + limit: body.limit, }); return res.status(200).json({ rows }); }), ); +router.use('/', require('../helpers').commonErrorHandler); + module.exports = router; diff --git a/backend/src/routes/student_guardians.js b/backend/src/routes/student_guardians.js index b67a7a1..1e140e9 100644 --- a/backend/src/routes/student_guardians.js +++ b/backend/src/routes/student_guardians.js @@ -3,10 +3,9 @@ const express = require('express'); const Student_guardiansService = require('../services/student_guardians'); const Student_guardiansDBApi = require('../db/api/student_guardians'); +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); const wrapAsync = require('../helpers').wrapAsync; -const config = require('../config'); - const router = express.Router(); @@ -303,6 +302,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); @@ -378,14 +378,14 @@ router.get('/autocomplete', async (req, res) => { const globalAccess = req.currentUser.app_role.globalAccess; - const organizationId = req.currentUser.organization?.id + const schoolId = getCurrentUserSchoolId(req.currentUser); const payload = await Student_guardiansDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - globalAccess, organizationId, + globalAccess, schoolId, ); res.status(200).send(payload); @@ -426,6 +426,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await Student_guardiansDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/students.js b/backend/src/routes/students.js index b727143..c8ff048 100644 --- a/backend/src/routes/students.js +++ b/backend/src/routes/students.js @@ -3,10 +3,9 @@ const express = require('express'); const StudentsService = require('../services/students'); const StudentsDBApi = require('../db/api/students'); +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); const wrapAsync = require('../helpers').wrapAsync; -const config = require('../config'); - const router = express.Router(); @@ -322,6 +321,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); @@ -397,14 +397,14 @@ router.get('/autocomplete', async (req, res) => { const globalAccess = req.currentUser.app_role.globalAccess; - const organizationId = req.currentUser.organization?.id + const schoolId = getCurrentUserSchoolId(req.currentUser); const payload = await StudentsDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - globalAccess, organizationId, + globalAccess, schoolId, ); res.status(200).send(payload); @@ -445,6 +445,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await StudentsDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/subjects.js b/backend/src/routes/subjects.js index 6b1b5ef..c5bdbe0 100644 --- a/backend/src/routes/subjects.js +++ b/backend/src/routes/subjects.js @@ -3,10 +3,9 @@ const express = require('express'); const SubjectsService = require('../services/subjects'); const SubjectsDBApi = require('../db/api/subjects'); +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); const wrapAsync = require('../helpers').wrapAsync; -const config = require('../config'); - const router = express.Router(); @@ -306,6 +305,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); @@ -381,14 +381,14 @@ router.get('/autocomplete', async (req, res) => { const globalAccess = req.currentUser.app_role.globalAccess; - const organizationId = req.currentUser.organization?.id + const schoolId = getCurrentUserSchoolId(req.currentUser); const payload = await SubjectsDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - globalAccess, organizationId, + globalAccess, schoolId, ); res.status(200).send(payload); @@ -429,6 +429,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await SubjectsDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/teachers.js b/backend/src/routes/teachers.js index 05a967a..2df63b7 100644 --- a/backend/src/routes/teachers.js +++ b/backend/src/routes/teachers.js @@ -3,10 +3,9 @@ const express = require('express'); const TeachersService = require('../services/teachers'); const TeachersDBApi = require('../db/api/teachers'); +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); const wrapAsync = require('../helpers').wrapAsync; -const config = require('../config'); - const router = express.Router(); @@ -315,6 +314,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); @@ -390,14 +390,14 @@ router.get('/autocomplete', async (req, res) => { const globalAccess = req.currentUser.app_role.globalAccess; - const organizationId = req.currentUser.organization?.id + const schoolId = getCurrentUserSchoolId(req.currentUser); const payload = await TeachersDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - globalAccess, organizationId, + globalAccess, schoolId, ); res.status(200).send(payload); @@ -438,6 +438,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await TeachersDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js index 69f227e..e40e47a 100644 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.js @@ -3,10 +3,9 @@ const express = require('express'); const UsersService = require('../services/users'); const UsersDBApi = require('../db/api/users'); +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); const wrapAsync = require('../helpers').wrapAsync; -const config = require('../config'); - const router = express.Router(); @@ -314,6 +313,7 @@ router.get('/', wrapAsync(async (req, res) => { } catch (err) { console.error(err); + throw err; } } else { res.status(200).send(payload); @@ -389,14 +389,14 @@ router.get('/autocomplete', async (req, res) => { const globalAccess = req.currentUser.app_role.globalAccess; - const organizationId = req.currentUser.organization?.id + const schoolId = getCurrentUserSchoolId(req.currentUser); const payload = await UsersDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, - globalAccess, organizationId, + globalAccess, schoolId, ); res.status(200).send(payload); @@ -437,6 +437,7 @@ router.get('/autocomplete', async (req, res) => { router.get('/:id', wrapAsync(async (req, res) => { const payload = await UsersDBApi.findBy( { id: req.params.id }, + { currentUser: req.currentUser }, ); diff --git a/backend/src/services/assessments.js b/backend/src/services/assessments.js index ff0c5c1..6d9d4b3 100644 --- a/backend/src/services/assessments.js +++ b/backend/src/services/assessments.js @@ -1,11 +1,7 @@ const db = require('../db/models'); const AssessmentsDBApi = require('../db/api/assessments'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); @@ -28,28 +24,13 @@ module.exports = class AssessmentsService { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }) + const results = await parseImportFile(req, res); await AssessmentsDBApi.bulkImport(results, { transaction, @@ -70,7 +51,7 @@ module.exports = class AssessmentsService { try { let assessments = await AssessmentsDBApi.findBy( {id}, - {transaction}, + { transaction, currentUser }, ); if (!assessments) { @@ -95,7 +76,7 @@ module.exports = class AssessmentsService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/backend/src/services/attendance.js b/backend/src/services/attendance.js index 2b6acae..9776c59 100644 --- a/backend/src/services/attendance.js +++ b/backend/src/services/attendance.js @@ -1,11 +1,7 @@ const db = require('../db/models'); const AttendanceDBApi = require('../db/api/attendance'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); @@ -28,28 +24,13 @@ module.exports = class AttendanceService { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }) + const results = await parseImportFile(req, res); await AttendanceDBApi.bulkImport(results, { transaction, @@ -70,7 +51,7 @@ module.exports = class AttendanceService { try { let attendance = await AttendanceDBApi.findBy( {id}, - {transaction}, + { transaction, currentUser }, ); if (!attendance) { @@ -95,7 +76,7 @@ module.exports = class AttendanceService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/backend/src/services/auth.js b/backend/src/services/auth.js index bcc3411..de47b6c 100644 --- a/backend/src/services/auth.js +++ b/backend/src/services/auth.js @@ -1,4 +1,5 @@ const UsersDBApi = require('../db/api/users'); +const db = require('../db/models'); const ValidationError = require('./notifications/errors/validation'); const ForbiddenError = require('./notifications/errors/forbidden'); const bcrypt = require('bcrypt'); @@ -83,7 +84,7 @@ class Auth { return helpers.jwtSign(data); } - static async signin(email, password, options = {}) { + static async signin(email, password) { const user = await UsersDBApi.findBy({email}); if (!user) { @@ -290,12 +291,21 @@ class Auth { try { await UsersDBApi.findBy( {id: currentUser.id}, - {transaction}, + { transaction, currentUser }, ); + const safeProfile = { + firstName: data?.firstName, + lastName: data?.lastName, + phoneNumber: data?.phoneNumber, + password: data?.password, + avatar: data?.avatar, + }; + await UsersDBApi.update( currentUser.id, - data, + safeProfile, + currentUser.app_role?.globalAccess, { currentUser, transaction diff --git a/backend/src/services/book_loans.js b/backend/src/services/book_loans.js index ffafeea..dd4b93c 100644 --- a/backend/src/services/book_loans.js +++ b/backend/src/services/book_loans.js @@ -1,11 +1,7 @@ const db = require('../db/models'); const Book_loansDBApi = require('../db/api/book_loans'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); @@ -28,28 +24,13 @@ module.exports = class Book_loansService { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }) + const results = await parseImportFile(req, res); await Book_loansDBApi.bulkImport(results, { transaction, @@ -70,7 +51,7 @@ module.exports = class Book_loansService { try { let book_loans = await Book_loansDBApi.findBy( {id}, - {transaction}, + { transaction, currentUser }, ); if (!book_loans) { @@ -95,7 +76,7 @@ module.exports = class Book_loansService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/backend/src/services/books.js b/backend/src/services/books.js index 94ca652..f76047f 100644 --- a/backend/src/services/books.js +++ b/backend/src/services/books.js @@ -1,11 +1,7 @@ const db = require('../db/models'); const BooksDBApi = require('../db/api/books'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); @@ -28,28 +24,13 @@ module.exports = class BooksService { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }) + const results = await parseImportFile(req, res); await BooksDBApi.bulkImport(results, { transaction, @@ -70,7 +51,7 @@ module.exports = class BooksService { try { let books = await BooksDBApi.findBy( {id}, - {transaction}, + { transaction, currentUser }, ); if (!books) { @@ -95,7 +76,7 @@ module.exports = class BooksService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/backend/src/services/classes.js b/backend/src/services/classes.js index 0c78aa4..4a57de5 100644 --- a/backend/src/services/classes.js +++ b/backend/src/services/classes.js @@ -1,11 +1,7 @@ const db = require('../db/models'); const ClassesDBApi = require('../db/api/classes'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); @@ -28,28 +24,13 @@ module.exports = class ClassesService { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }) + const results = await parseImportFile(req, res); await ClassesDBApi.bulkImport(results, { transaction, @@ -70,7 +51,7 @@ module.exports = class ClassesService { try { let classes = await ClassesDBApi.findBy( {id}, - {transaction}, + { transaction, currentUser }, ); if (!classes) { @@ -95,7 +76,7 @@ module.exports = class ClassesService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/backend/src/services/courses.js b/backend/src/services/courses.js index a63b30c..7969322 100644 --- a/backend/src/services/courses.js +++ b/backend/src/services/courses.js @@ -1,11 +1,7 @@ const db = require('../db/models'); const CoursesDBApi = require('../db/api/courses'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); @@ -28,28 +24,13 @@ module.exports = class CoursesService { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }) + const results = await parseImportFile(req, res); await CoursesDBApi.bulkImport(results, { transaction, @@ -70,7 +51,7 @@ module.exports = class CoursesService { try { let courses = await CoursesDBApi.findBy( {id}, - {transaction}, + { transaction, currentUser }, ); if (!courses) { @@ -95,7 +76,7 @@ module.exports = class CoursesService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/backend/src/services/employees.js b/backend/src/services/employees.js index 6e3afc4..29dcd09 100644 --- a/backend/src/services/employees.js +++ b/backend/src/services/employees.js @@ -1,11 +1,7 @@ const db = require('../db/models'); const EmployeesDBApi = require('../db/api/employees'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); @@ -28,28 +24,13 @@ module.exports = class EmployeesService { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }) + const results = await parseImportFile(req, res); await EmployeesDBApi.bulkImport(results, { transaction, @@ -70,7 +51,7 @@ module.exports = class EmployeesService { try { let employees = await EmployeesDBApi.findBy( {id}, - {transaction}, + { transaction, currentUser }, ); if (!employees) { @@ -95,7 +76,7 @@ module.exports = class EmployeesService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/backend/src/services/enrollments.js b/backend/src/services/enrollments.js index 7dde01e..01553a3 100644 --- a/backend/src/services/enrollments.js +++ b/backend/src/services/enrollments.js @@ -1,11 +1,7 @@ const db = require('../db/models'); const EnrollmentsDBApi = require('../db/api/enrollments'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); @@ -28,28 +24,13 @@ module.exports = class EnrollmentsService { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }) + const results = await parseImportFile(req, res); await EnrollmentsDBApi.bulkImport(results, { transaction, @@ -70,7 +51,7 @@ module.exports = class EnrollmentsService { try { let enrollments = await EnrollmentsDBApi.findBy( {id}, - {transaction}, + { transaction, currentUser }, ); if (!enrollments) { @@ -95,7 +76,7 @@ module.exports = class EnrollmentsService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/backend/src/services/file.js b/backend/src/services/file.js index 597be30..db8e39a 100644 --- a/backend/src/services/file.js +++ b/backend/src/services/file.js @@ -2,7 +2,172 @@ const formidable = require('formidable'); const fs = require('fs'); const config = require('../config'); const path = require('path'); -const { format } = require("util"); +const crypto = require('crypto'); +const db = require('../db/models'); + +const POSIX_SEPARATOR = '/'; + +const getFirstValue = (value) => { + if (Array.isArray(value)) { + return value[0]; + } + + return value; +}; + +const normalizeStoragePath = (value) => { + if (typeof value !== 'string') { + return null; + } + + const trimmed = value.trim(); + + if (!trimmed || trimmed.includes('\0') || trimmed.includes('\\')) { + return null; + } + + const normalized = path.posix.normalize(trimmed); + + if ( + normalized === '.' || + path.posix.isAbsolute(normalized) || + normalized === '..' || + normalized.startsWith(`..${POSIX_SEPARATOR}`) || + normalized.split(POSIX_SEPARATOR).includes('..') || + normalized !== trimmed + ) { + return null; + } + + return normalized; +}; + +const normalizeFolder = (folder) => normalizeStoragePath(folder); + +const normalizePrivateUrl = (privateUrl) => normalizeStoragePath(privateUrl); + +const normalizeFilename = (filename) => { + if (typeof filename !== 'string') { + return null; + } + + const trimmed = filename.trim(); + + if ( + !trimmed || + trimmed.includes('\0') || + trimmed.includes(POSIX_SEPARATOR) || + trimmed.includes('\\') || + trimmed === '.' || + trimmed === '..' || + path.basename(trimmed) !== trimmed + ) { + return null; + } + + return trimmed; +}; + +const createDownloadToken = (privateUrl) => ( + crypto + .createHmac('sha256', config.secret_key) + .update(privateUrl) + .digest('hex') +); + +const isValidDownloadToken = (privateUrl, token) => { + if (typeof token !== 'string' || !token) { + return false; + } + + const expected = createDownloadToken(privateUrl); + const tokenBuffer = Buffer.from(token, 'hex'); + const expectedBuffer = Buffer.from(expected, 'hex'); + + return ( + tokenBuffer.length === expectedBuffer.length && + crypto.timingSafeEqual(tokenBuffer, expectedBuffer) + ); +}; + +const buildPublicDownloadUrl = (privateUrl) => ( + `/api/file/download?privateUrl=${encodeURIComponent(privateUrl)}&token=${createDownloadToken(privateUrl)}` +); + +const ensureKnownPrivateUrl = async (privateUrl) => { + const file = await db.file.findOne({ + where: { privateUrl }, + attributes: ['id'], + }); + + return !!file; +}; + +const canDownloadPrivateUrl = async (privateUrl, token) => { + if (isValidDownloadToken(privateUrl, token)) { + return true; + } + + return ensureKnownPrivateUrl(privateUrl); +}; + +const assertSafeLocalPath = (privateUrl) => { + const baseDir = path.resolve(config.uploadDir); + const targetPath = path.resolve(baseDir, privateUrl); + + if (targetPath !== baseDir && !targetPath.startsWith(`${baseDir}${path.sep}`)) { + return null; + } + + return targetPath; +}; + +const validateFileMetadata = async (file, relation, options = {}) => { + const privateUrl = normalizePrivateUrl(file && file.privateUrl); + const expectedFolder = normalizeFolder( + `${String(relation.belongsTo)}/${String(relation.belongsToColumn)}`, + ); + + if (!privateUrl || !expectedFolder) { + const error = new Error('Invalid file path.'); + error.code = 400; + throw error; + } + + const expectedPrefix = `${expectedFolder}/`; + + if (!privateUrl.startsWith(expectedPrefix)) { + const error = new Error('Invalid file relation path.'); + error.code = 400; + throw error; + } + + const filename = privateUrl.slice(expectedPrefix.length); + + if (!normalizeFilename(filename)) { + const error = new Error('Invalid file name.'); + error.code = 400; + throw error; + } + + const existingFile = await db.file.findOne({ + where: { privateUrl }, + attributes: ['id'], + transaction: options.transaction, + }); + + if (existingFile) { + const error = new Error('File already belongs to a record.'); + error.code = 403; + throw error; + } + + return { + ...file, + privateUrl, + publicUrl: buildPublicDownloadUrl(privateUrl), + }; +}; const ensureDirectoryExistence = (filePath) => { const dirname = path.dirname(filePath); @@ -36,20 +201,29 @@ const uploadLocal = ( return; } + let uploadFolder = folder; + if (validations.folderIncludesAuthenticationUid) { - folder = folder.replace( + uploadFolder = uploadFolder.replace( ':userId', req.currentUser.authenticationUid, ); if ( !req.currentUser.authenticationUid || - !folder.includes(req.currentUser.authenticationUid) + !uploadFolder.includes(req.currentUser.authenticationUid) ) { res.sendStatus(403); return; } } + uploadFolder = normalizeFolder(uploadFolder); + + if (!uploadFolder) { + res.status(400).send({ message: 'Invalid upload path.' }); + return; + } + const form = new formidable.IncomingForm(); form.uploadDir = config.uploadDir; @@ -58,44 +232,83 @@ const uploadLocal = ( } form.parse(req, function (err, fields, files) { - const filename = String(fields.filename); - const fileTempUrl = files.file.path; - - if (!filename) { - fs.unlinkSync(fileTempUrl); - res.sendStatus(500); + if (err) { + console.error('Upload parse error:', err); + res.status(500).send(err); return; } - const privateUrl = path.join( - form.uploadDir, - folder, - filename, + const filename = normalizeFilename(String(getFirstValue(fields.filename) || '')); + const uploadedFile = getFirstValue(files.file); + const fileTempUrl = uploadedFile && uploadedFile.path; + + if (!filename || !fileTempUrl) { + if (fileTempUrl && fs.existsSync(fileTempUrl)) { + fs.unlinkSync(fileTempUrl); + } + res.status(400).send({ message: 'Invalid uploaded file.' }); + return; + } + + const privateUrl = assertSafeLocalPath( + path.posix.join(uploadFolder, filename), ); + + if (!privateUrl) { + fs.unlinkSync(fileTempUrl); + res.status(400).send({ message: 'Invalid upload path.' }); + return; + } + ensureDirectoryExistence(privateUrl); fs.renameSync(fileTempUrl, privateUrl); - res.sendStatus(200); + res.status(200).send({ + url: buildPublicDownloadUrl(path.posix.join(uploadFolder, filename)), + }); }); form.on('error', function (err) { + console.error('Upload form error:', err); res.status(500).send(err); }); } } const downloadLocal = async (req, res) => { - const privateUrl = req.query.privateUrl; + const privateUrl = normalizePrivateUrl(getFirstValue(req.query.privateUrl)); if (!privateUrl) { return res.sendStatus(404); } - res.download(path.join(config.uploadDir, privateUrl)); + + const canDownload = await canDownloadPrivateUrl( + privateUrl, + getFirstValue(req.query.token), + ); + if (!canDownload) { + return res.sendStatus(404); + } + + const privatePath = assertSafeLocalPath(privateUrl); + if (!privatePath) { + return res.sendStatus(404); + } + + res.download(privatePath, (err) => { + if (!err) { + return; + } + + console.error('Download error:', err); + if (!res.headersSent) { + res.sendStatus(404); + } + }); } const initGCloud = () => { const processFile = require("../middlewares/upload"); const { Storage } = require("@google-cloud/storage"); - const crypto = require('crypto') const hash = config.gcloud.hash const privateKey = process.env.GC_PRIVATE_KEY.replace(/\\\n/g, "\n"); @@ -116,14 +329,20 @@ const uploadGCloud = async (folder, req, res) => { try { const {hash, bucket, processFile} = initGCloud(); await processFile(req, res); - let buffer = await req.file.buffer; - let filename = await req.body.filename; + const uploadFolder = normalizeFolder(folder); if (!req.file) { return res.status(400).send({ message: "Please upload a file!" }); } - let path = `${hash}/${folder}/${filename}`; + let buffer = await req.file.buffer; + let filename = normalizeFilename(await req.body.filename); + + if (!uploadFolder || !filename) { + return res.status(400).send({ message: 'Invalid upload path.' }); + } + + let path = `${hash}/${uploadFolder}/${filename}`; let blob = bucket.file(path); console.log(path); @@ -140,10 +359,9 @@ const uploadGCloud = async (folder, req, res) => { console.log(`https://storage.googleapis.com/${bucket.name}/${blob.name}`); - blobStream.on("finish", async (data) => { - const publicUrl = format( - `https://storage.googleapis.com/${bucket.name}/${blob.name}` - ); + blobStream.on("finish", async () => { + const privateUrl = path.posix.join(uploadFolder, filename); + const publicUrl = buildPublicDownloadUrl(privateUrl); res.status(200).send({ message: "Uploaded the file successfully: " + path, @@ -163,9 +381,21 @@ const uploadGCloud = async (folder, req, res) => { const downloadGCloud = async (req, res) => { try { - const {hash, bucket, processFile} = initGCloud(); + const {hash, bucket} = initGCloud(); + + const privateUrl = normalizePrivateUrl(getFirstValue(await req.query.privateUrl)); + if (!privateUrl) { + return res.sendStatus(404); + } + + const canDownload = await canDownloadPrivateUrl( + privateUrl, + getFirstValue(req.query.token), + ); + if (!canDownload) { + return res.sendStatus(404); + } - const privateUrl = await req.query.privateUrl; const filePath = `${hash}/${privateUrl}`; const file = bucket.file(filePath) const fileExists = await file.exists(); @@ -176,7 +406,7 @@ const downloadGCloud = async (req, res) => { } else { res.status(404).send({ - message: "Could not download the file. " + err, + message: "Could not download the file.", }); } } catch (err) { @@ -188,8 +418,13 @@ const downloadGCloud = async (req, res) => { const deleteGCloud = async (privateUrl) => { try { - const {hash, bucket, processFile} = initGCloud(); - const filePath = `${hash}/${privateUrl}`; + const normalizedPrivateUrl = normalizePrivateUrl(privateUrl); + if (!normalizedPrivateUrl) { + return; + } + + const {hash, bucket} = initGCloud(); + const filePath = `${hash}/${normalizedPrivateUrl}`; const file = bucket.file(filePath) const fileExists = await file.exists(); @@ -208,6 +443,13 @@ module.exports = { downloadLocal, deleteGCloud, uploadGCloud, - downloadGCloud + downloadGCloud, + normalizePrivateUrl, + normalizeFilename, + normalizeFolder, + buildPublicDownloadUrl, + validateFileMetadata, + createDownloadToken, + isValidDownloadToken, } diff --git a/backend/src/services/grades.js b/backend/src/services/grades.js index 2b9bdaa..3a6b037 100644 --- a/backend/src/services/grades.js +++ b/backend/src/services/grades.js @@ -1,11 +1,7 @@ const db = require('../db/models'); const GradesDBApi = require('../db/api/grades'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); @@ -28,28 +24,13 @@ module.exports = class GradesService { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }) + const results = await parseImportFile(req, res); await GradesDBApi.bulkImport(results, { transaction, @@ -70,7 +51,7 @@ module.exports = class GradesService { try { let grades = await GradesDBApi.findBy( {id}, - {transaction}, + { transaction, currentUser }, ); if (!grades) { @@ -95,7 +76,7 @@ module.exports = class GradesService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/backend/src/services/guardians.js b/backend/src/services/guardians.js index 956b88b..d076f6d 100644 --- a/backend/src/services/guardians.js +++ b/backend/src/services/guardians.js @@ -1,11 +1,7 @@ const db = require('../db/models'); const GuardiansDBApi = require('../db/api/guardians'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); @@ -28,28 +24,13 @@ module.exports = class GuardiansService { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }) + const results = await parseImportFile(req, res); await GuardiansDBApi.bulkImport(results, { transaction, @@ -70,7 +51,7 @@ module.exports = class GuardiansService { try { let guardians = await GuardiansDBApi.findBy( {id}, - {transaction}, + { transaction, currentUser }, ); if (!guardians) { @@ -95,7 +76,7 @@ module.exports = class GuardiansService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/backend/src/services/importFileParser.js b/backend/src/services/importFileParser.js new file mode 100644 index 0000000..ea65707 --- /dev/null +++ b/backend/src/services/importFileParser.js @@ -0,0 +1,380 @@ +const csv = require('csv-parser'); +const stream = require('stream'); +const zlib = require('zlib'); +const processFile = require('../middlewares/upload'); + +function createBadRequest(message) { + const error = new Error(message); + error.code = 400; + return error; +} + +function getExtension(filename) { + if (!filename) { + return ''; + } + + const match = String(filename).match(/\.([^.]+)$/); + return match ? match[1].toLowerCase() : ''; +} + +async function parseCsvBuffer(buffer) { + const bufferStream = new stream.PassThrough(); + const results = []; + + bufferStream.end(Buffer.from(buffer, 'utf-8')); + + await new Promise((resolve, reject) => { + bufferStream + .pipe(csv()) + .on('data', (data) => results.push(data)) + .on('end', resolve) + .on('error', reject); + }); + + return results; +} + +function findEndOfCentralDirectory(buffer) { + const signature = 0x06054b50; + const minOffset = Math.max(0, buffer.length - 0xffff - 22); + + for (let offset = buffer.length - 22; offset >= minOffset; offset -= 1) { + if (buffer.readUInt32LE(offset) === signature) { + return offset; + } + } + + throw createBadRequest('Invalid XLSX file.'); +} + +function readZipEntries(buffer) { + const centralDirectorySignature = 0x02014b50; + const localFileSignature = 0x04034b50; + const endOffset = findEndOfCentralDirectory(buffer); + const entriesCount = buffer.readUInt16LE(endOffset + 10); + let offset = buffer.readUInt32LE(endOffset + 16); + const entries = {}; + + for (let index = 0; index < entriesCount; index += 1) { + if (buffer.readUInt32LE(offset) !== centralDirectorySignature) { + throw createBadRequest('Invalid XLSX file.'); + } + + const compressionMethod = buffer.readUInt16LE(offset + 10); + const compressedSize = buffer.readUInt32LE(offset + 20); + const fileNameLength = buffer.readUInt16LE(offset + 28); + const extraFieldLength = buffer.readUInt16LE(offset + 30); + const fileCommentLength = buffer.readUInt16LE(offset + 32); + const localHeaderOffset = buffer.readUInt32LE(offset + 42); + const fileName = buffer.toString('utf8', offset + 46, offset + 46 + fileNameLength); + + if (buffer.readUInt32LE(localHeaderOffset) !== localFileSignature) { + throw createBadRequest('Invalid XLSX file.'); + } + + const localFileNameLength = buffer.readUInt16LE(localHeaderOffset + 26); + const localExtraFieldLength = buffer.readUInt16LE(localHeaderOffset + 28); + const dataOffset = localHeaderOffset + 30 + localFileNameLength + localExtraFieldLength; + const compressedData = buffer.slice(dataOffset, dataOffset + compressedSize); + + let data; + if (compressionMethod === 0) { + data = compressedData; + } else if (compressionMethod === 8) { + data = zlib.inflateRawSync(compressedData); + } else { + throw createBadRequest('Unsupported XLSX compression method.'); + } + + entries[fileName] = data; + offset += 46 + fileNameLength + extraFieldLength + fileCommentLength; + } + + return entries; +} + +function xmlText(entries, filename) { + return entries[filename] ? entries[filename].toString('utf8') : ''; +} + +function decodeXml(value) { + if (value === undefined || value === null) { + return ''; + } + + return String(value) + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/&/g, '&'); +} + +function getAttribute(source, name) { + const escapedName = name.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); + const match = source.match(new RegExp(`(?:^|\\s)${escapedName}=(['"])(.*?)\\1`)); + return match ? decodeXml(match[2]) : null; +} + +function extractTextFromXmlFragment(fragment) { + const parts = []; + const textRegex = /]*>([\s\S]*?)<\/t>/g; + let textMatch; + + while ((textMatch = textRegex.exec(fragment)) !== null) { + parts.push(decodeXml(textMatch[1])); + } + + if (parts.length) { + return parts.join(''); + } + + return decodeXml(fragment.replace(/<[^>]+>/g, '')); +} + +function parseSharedStrings(sharedStringsXml) { + const sharedStrings = []; + const itemRegex = /]*>([\s\S]*?)<\/si>/g; + let itemMatch; + + while ((itemMatch = itemRegex.exec(sharedStringsXml)) !== null) { + sharedStrings.push(extractTextFromXmlFragment(itemMatch[1])); + } + + return sharedStrings; +} + +function parseDateStyleIndexes(stylesXml) { + if (!stylesXml) { + return new Set(); + } + + const builtInDateFormats = new Set([ + 14, 15, 16, 17, 18, 19, 20, 21, 22, + 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, + 45, 46, 47, 50, 51, 52, 53, 54, 55, 56, 57, 58, + ]); + const customDateFormats = new Set(); + const numberFormatRegex = /]*)\/>/g; + let numberFormatMatch; + + while ((numberFormatMatch = numberFormatRegex.exec(stylesXml)) !== null) { + const numberFormatId = Number(getAttribute(numberFormatMatch[1], 'numFmtId')); + const formatCode = getAttribute(numberFormatMatch[1], 'formatCode') || ''; + + if (Number.isFinite(numberFormatId) && /[dy]/i.test(formatCode)) { + customDateFormats.add(numberFormatId); + } + } + + const cellFormatsMatch = stylesXml.match(/]*>([\s\S]*?)<\/cellXfs>/); + if (!cellFormatsMatch) { + return new Set(); + } + + const styleIndexes = new Set(); + const formatRegex = /]*)\/?>(?:<\/xf>)?/g; + let styleIndex = 0; + let formatMatch; + + while ((formatMatch = formatRegex.exec(cellFormatsMatch[1])) !== null) { + const numberFormatId = Number(getAttribute(formatMatch[1], 'numFmtId')); + + if (builtInDateFormats.has(numberFormatId) || customDateFormats.has(numberFormatId)) { + styleIndexes.add(styleIndex); + } + + styleIndex += 1; + } + + return styleIndexes; +} + +function excelSerialDateToIsoDate(value) { + const serial = Number(value); + + if (!Number.isFinite(serial)) { + return value; + } + + const milliseconds = Math.round((serial - 25569) * 86400 * 1000); + const date = new Date(milliseconds); + + if (Number.isNaN(date.getTime())) { + return value; + } + + return date.toISOString().slice(0, 10); +} + +function columnNameToIndex(columnName) { + return columnName + .toUpperCase() + .split('') + .reduce((result, char) => result * 26 + char.charCodeAt(0) - 64, 0) - 1; +} + +function getFirstSheetPath(entries) { + const workbookXml = xmlText(entries, 'xl/workbook.xml'); + const relationshipsXml = xmlText(entries, 'xl/_rels/workbook.xml.rels'); + + if (!workbookXml || !relationshipsXml) { + return entries['xl/worksheets/sheet1.xml'] ? 'xl/worksheets/sheet1.xml' : null; + } + + const sheetMatch = workbookXml.match(/]*)\/?>/); + const relationshipId = sheetMatch ? getAttribute(sheetMatch[1], 'r:id') : null; + + if (!relationshipId) { + return entries['xl/worksheets/sheet1.xml'] ? 'xl/worksheets/sheet1.xml' : null; + } + + const relationshipRegex = /]*)\/?>/g; + let relationshipMatch; + + while ((relationshipMatch = relationshipRegex.exec(relationshipsXml)) !== null) { + if (getAttribute(relationshipMatch[1], 'Id') === relationshipId) { + const target = getAttribute(relationshipMatch[1], 'Target'); + + if (!target) { + break; + } + + if (target.startsWith('/')) { + return target.replace(/^\//, ''); + } + + return `xl/${target}`.replace(/\/\.\//g, '/'); + } + } + + return entries['xl/worksheets/sheet1.xml'] ? 'xl/worksheets/sheet1.xml' : null; +} + +function readCellValue(cellAttributes, cellBody, sharedStrings, dateStyleIndexes) { + const cellType = getAttribute(cellAttributes, 't'); + const styleIndex = Number(getAttribute(cellAttributes, 's')); + + if (cellType === 'inlineStr') { + return extractTextFromXmlFragment(cellBody); + } + + const valueMatch = cellBody.match(/]*>([\s\S]*?)<\/v>/); + const rawValue = valueMatch ? decodeXml(valueMatch[1]) : ''; + + if (cellType === 's') { + return sharedStrings[Number(rawValue)] || ''; + } + + if (cellType === 'b') { + return rawValue === '1' ? 'true' : 'false'; + } + + if (dateStyleIndexes.has(styleIndex) && rawValue !== '') { + return excelSerialDateToIsoDate(rawValue); + } + + return rawValue; +} + +function parseWorksheetRows(sheetXml, sharedStrings, dateStyleIndexes) { + const rows = []; + const rowRegex = /]*>([\s\S]*?)<\/row>/g; + let rowMatch; + + while ((rowMatch = rowRegex.exec(sheetXml)) !== null) { + const row = []; + const cellRegex = /]*?)>([\s\S]*?)<\/c>|]*?)\/>/g; + let cellMatch; + let nextColumnIndex = 0; + + while ((cellMatch = cellRegex.exec(rowMatch[1])) !== null) { + const cellAttributes = cellMatch[1] || cellMatch[3] || ''; + const cellBody = cellMatch[2] || ''; + const cellReference = getAttribute(cellAttributes, 'r'); + const columnMatch = cellReference ? cellReference.match(/[A-Z]+/i) : null; + const columnIndex = columnMatch ? columnNameToIndex(columnMatch[0]) : nextColumnIndex; + + row[columnIndex] = readCellValue(cellAttributes, cellBody, sharedStrings, dateStyleIndexes); + nextColumnIndex = columnIndex + 1; + } + + if (row.some((value) => value !== undefined && value !== '')) { + rows.push(row); + } + } + + return rows; +} + +function rowsToObjects(rows) { + if (!rows.length) { + return []; + } + + const headers = rows[0].map((header) => String(header || '').replace(/^\uFEFF/, '').trim()); + + return rows.slice(1).reduce((items, row) => { + const item = {}; + + headers.forEach((header, index) => { + if (!header) { + return; + } + + const value = row[index]; + item[header] = value === undefined ? '' : value; + }); + + if (Object.values(item).some((value) => value !== '')) { + items.push(item); + } + + return items; + }, []); +} + +function parseXlsxBuffer(buffer) { + const entries = readZipEntries(buffer); + const sheetPath = getFirstSheetPath(entries); + + if (!sheetPath || !entries[sheetPath]) { + throw createBadRequest('XLSX file does not contain a worksheet.'); + } + + const sharedStrings = parseSharedStrings(xmlText(entries, 'xl/sharedStrings.xml')); + const dateStyleIndexes = parseDateStyleIndexes(xmlText(entries, 'xl/styles.xml')); + const rows = parseWorksheetRows(xmlText(entries, sheetPath), sharedStrings, dateStyleIndexes); + + return rowsToObjects(rows); +} + +async function parseImportFile(req, res) { + await processFile(req, res); + + if (!req.file || !req.file.buffer) { + throw createBadRequest('No import file was uploaded.'); + } + + const filename = req.file.originalname || req.body.filename || ''; + const extension = getExtension(filename); + + if (extension === 'csv') { + return parseCsvBuffer(req.file.buffer); + } + + if (extension === 'xlsx') { + return parseXlsxBuffer(req.file.buffer); + } + + if (extension === 'xls') { + throw createBadRequest('Legacy .xls imports are not supported yet. Please save the spreadsheet as .xlsx or .csv.'); + } + + throw createBadRequest('Invalid import format. Allowed formats: .csv, .xlsx.'); +} + +module.exports = parseImportFile; +module.exports.parseCsvBuffer = parseCsvBuffer; +module.exports.parseXlsxBuffer = parseXlsxBuffer; diff --git a/backend/src/services/invoices.js b/backend/src/services/invoices.js index b0356af..040dff4 100644 --- a/backend/src/services/invoices.js +++ b/backend/src/services/invoices.js @@ -1,11 +1,7 @@ const db = require('../db/models'); const InvoicesDBApi = require('../db/api/invoices'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); @@ -28,28 +24,13 @@ module.exports = class InvoicesService { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }) + const results = await parseImportFile(req, res); await InvoicesDBApi.bulkImport(results, { transaction, @@ -70,7 +51,7 @@ module.exports = class InvoicesService { try { let invoices = await InvoicesDBApi.findBy( {id}, - {transaction}, + { transaction, currentUser }, ); if (!invoices) { @@ -95,7 +76,7 @@ module.exports = class InvoicesService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/backend/src/services/openai.js b/backend/src/services/openai.js index 3793398..6d3ee90 100644 --- a/backend/src/services/openai.js +++ b/backend/src/services/openai.js @@ -2,20 +2,10 @@ const axios = require('axios'); const config = require('../config'); const { LocalAIApi } = require('../ai/LocalAIApi'); -const loadRoleService = () => { - try { - return require('./roles'); - } catch (error) { - console.error('Role service is missing. Advanced roles are required for this operation.', error); - const err = new Error('Role service is missing. Advanced roles are required for this operation.'); - err.originalError = error; - throw err; - } -}; + module.exports = class OpenAiService { - static async getWidget(payload, userId, roleId) { - const RoleService = loadRoleService(); + static async getWidget(payload) { const response = await axios.post( `${config.flHost}/${config.project_uuid}/project_customization_widgets.json`, payload, @@ -23,7 +13,6 @@ module.exports = class OpenAiService { if (response.status >= 200 && response.status < 300) { const { widget_id } = await response.data; - await RoleService.addRoleInfo(roleId, userId, 'widgets', widget_id); return widget_id; } else { console.error('=======error=======', response.data); diff --git a/backend/src/services/payments.js b/backend/src/services/payments.js index ef655fe..22f8754 100644 --- a/backend/src/services/payments.js +++ b/backend/src/services/payments.js @@ -1,11 +1,7 @@ const db = require('../db/models'); const PaymentsDBApi = require('../db/api/payments'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); @@ -28,28 +24,13 @@ module.exports = class PaymentsService { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }) + const results = await parseImportFile(req, res); await PaymentsDBApi.bulkImport(results, { transaction, @@ -70,7 +51,7 @@ module.exports = class PaymentsService { try { let payments = await PaymentsDBApi.findBy( {id}, - {transaction}, + { transaction, currentUser }, ); if (!payments) { @@ -95,7 +76,7 @@ module.exports = class PaymentsService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/backend/src/services/permissions.js b/backend/src/services/permissions.js index e505d0c..24bceef 100644 --- a/backend/src/services/permissions.js +++ b/backend/src/services/permissions.js @@ -1,11 +1,7 @@ const db = require('../db/models'); const PermissionsDBApi = require('../db/api/permissions'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); @@ -28,28 +24,13 @@ module.exports = class PermissionsService { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }) + const results = await parseImportFile(req, res); await PermissionsDBApi.bulkImport(results, { transaction, @@ -95,7 +76,7 @@ module.exports = class PermissionsService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/backend/src/services/products.js b/backend/src/services/products.js index 458d58f..a3c18f1 100644 --- a/backend/src/services/products.js +++ b/backend/src/services/products.js @@ -1,11 +1,7 @@ const db = require('../db/models'); const ProductsDBApi = require('../db/api/products'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); @@ -28,28 +24,13 @@ module.exports = class ProductsService { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }) + const results = await parseImportFile(req, res); await ProductsDBApi.bulkImport(results, { transaction, @@ -70,7 +51,7 @@ module.exports = class ProductsService { try { let products = await ProductsDBApi.findBy( {id}, - {transaction}, + { transaction, currentUser }, ); if (!products) { @@ -95,7 +76,7 @@ module.exports = class ProductsService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/backend/src/services/roles.js b/backend/src/services/roles.js index ec6186b..63d87ed 100644 --- a/backend/src/services/roles.js +++ b/backend/src/services/roles.js @@ -1,19 +1,41 @@ const db = require('../db/models'); const RolesDBApi = require('../db/api/roles'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); const axios = require('axios'); const config = require('../config'); -const stream = require('stream'); +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); +const { executeReadOnlySelect } = require('./sqlSafety'); + +const WIDGET_CUSTOMIZATION_KEY = 'widgets'; +const UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + +function badRequest(message) { + const error = new Error(message); + error.code = 400; + return error; +} + +function assertWidgetKey(key) { + if (key !== WIDGET_CUSTOMIZATION_KEY) { + throw badRequest('Invalid role customization key.'); + } +} + +function assertUuid(value, label) { + if (typeof value !== 'string' || !UUID_REGEX.test(value)) { + throw badRequest(`${label} must be a valid UUID.`); + } +} -function buildWidgetResult(widget, queryResult, queryString) { - if (queryResult[0] && queryResult[0].length) { - const key = Object.keys(queryResult[0][0])[0]; - const value = widget.widget_type === 'scalar' ? queryResult[0][0][key] : queryResult[0]; + +function buildWidgetResult(widget, rows, queryString) { + if (Array.isArray(rows) && rows.length) { + const key = Object.keys(rows[0])[0]; + const value = widget.widget_type === 'scalar' ? rows[0][key] : rows; const widgetData = JSON.parse(widget.data); return { ...widget, ...widgetData, value, query: queryString }; } else { @@ -21,14 +43,16 @@ function buildWidgetResult(widget, queryResult, queryString) { } } -async function executeQuery(queryString, currentUser) { +async function executeQuery(queryString, replacements) { try { - return await db.sequelize.query(queryString, { - replacements: { organizationId: currentUser.organizationId }, + return await executeReadOnlySelect(queryString, { + replacements, + maxRows: 1000, + timeoutMs: 5000, }); } catch (e) { - console.log(e); - return []; + console.error('Widget SQL execution failed:', e); + throw e; } } @@ -49,19 +73,36 @@ function insertWhereConditions(queryString, whereConditions) { } function constructWhereConditions(mainTable, currentUser, replacements) { - const { organizationId, app_role: { globalAccess } } = currentUser; - const tablesWithoutOrgId = ['permissions', 'roles']; - let whereConditions = ''; + const globalAccess = currentUser?.app_role?.globalAccess; + const tablesWithoutSchoolScope = ['permissions', 'roles']; + const model = db[mainTable]; + const rawAttributes = model?.rawAttributes || {}; + const conditions = []; - if (!globalAccess && !tablesWithoutOrgId.includes(mainTable)) { - whereConditions += `"${mainTable}"."organizationId" = :organizationId`; - replacements.organizationId = organizationId; + if (!globalAccess && !tablesWithoutSchoolScope.includes(mainTable)) { + const schoolId = getCurrentUserSchoolId(currentUser); + + if (!schoolId) { + conditions.push('1 = 0'); + } else if (mainTable === 'schools') { + conditions.push(`"${mainTable}"."id" = :schoolId`); + replacements.schoolId = schoolId; + } else if (rawAttributes.schoolId) { + conditions.push(`"${mainTable}"."schoolId" = :schoolId`); + replacements.schoolId = schoolId; + } else if (rawAttributes.schoolsId) { + conditions.push(`"${mainTable}"."schoolsId" = :schoolId`); + replacements.schoolId = schoolId; + } else { + conditions.push('1 = 0'); + } } - whereConditions += whereConditions ? ' AND ' : ''; - whereConditions += `"${mainTable}"."deletedAt" IS NULL`; + if (rawAttributes.deletedAt) { + conditions.push(`"${mainTable}"."deletedAt" IS NULL`); + } - return whereConditions; + return conditions.join(' AND '); } function extractTableName(queryString) { @@ -70,23 +111,22 @@ function extractTableName(queryString) { return match ? match[2] : null; } -function buildQueryString(widget, currentUser) { +function buildQuery(widget, currentUser) { let queryString = widget?.query || ''; const tableName = extractTableName(queryString); const mainTable = JSON.parse(widget?.data)?.main_table || tableName; const replacements = {}; const whereConditions = constructWhereConditions(mainTable, currentUser, replacements); queryString = insertWhereConditions(queryString, whereConditions); - console.log(queryString, 'queryString'); - return queryString; + return { queryString, replacements }; } async function constructWidgetsResults(widgets, currentUser) { const widgetsResults = []; for (const widget of widgets) { if (!widget) continue; - const queryString = buildQueryString(widget, currentUser); - const queryResult = await executeQuery(queryString, currentUser); + const { queryString, replacements } = buildQuery(widget, currentUser); + const queryResult = await executeQuery(queryString, replacements); widgetsResults.push(buildWidgetResult(widget, queryResult, queryString)); } return widgetsResults; @@ -97,6 +137,10 @@ async function fetchWidgetsData(widgets) { axios.get(`${config.flHost}/${config.project_uuid}/project_customization_widgets/${widgetId}.json`) ); const widgetResults = widgetPromises ? await Promise.allSettled(widgetPromises) : []; + widgetResults + .filter(result => result.status === 'rejected') + .forEach(result => console.error('Widget fetch failed:', result.reason)); + return widgetResults .filter(result => result.status === 'fulfilled') .map(result => result.value.data); @@ -116,7 +160,7 @@ function parseCustomization(role) { } } -async function findRole(roleId, currentUser) { +async function findRole(roleId) { const transaction = await db.sequelize.transaction(); try { const role = roleId @@ -148,28 +192,13 @@ module.exports = class RolesService { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }) + const results = await parseImportFile(req, res); await RolesDBApi.bulkImport(results, { transaction, @@ -215,7 +244,7 @@ module.exports = class RolesService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); @@ -254,22 +283,22 @@ module.exports = class RolesService { static async addRoleInfo(roleId, userId, key, widgetId, currentUser) { - const regexExpForUuid = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi; - const widgetIdIsUUID = regexExpForUuid.test(widgetId); + assertWidgetKey(key); + assertUuid(widgetId, 'Widget ID'); const transaction = await db.sequelize.transaction(); - let role; - if (roleId) { - role = await RolesDBApi.findBy({ id: roleId }, { transaction }); - } else { - role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); - } - - if (!role) { - throw new ValidationError('rolesNotFound'); - } - try { + let role; + if (roleId) { + role = await RolesDBApi.findBy({ id: roleId }, { transaction }); + } else { + role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); + } + + if (!role) { + throw new ValidationError('rolesNotFound'); + } + let customization = {}; try { customization = JSON.parse(role.role_customization || '{}'); @@ -277,12 +306,12 @@ module.exports = class RolesService { console.log(e); } - if (widgetIdIsUUID && Array.isArray(customization[key])) { + if (Array.isArray(customization[key])) { const el = customization[key].find((e) => e === widgetId); - !el ? customization[key].unshift(widgetId) : null; - } - - if (widgetIdIsUUID && !customization[key]) { + if (!el) { + customization[key].unshift(widgetId); + } + } else { customization[key] = [widgetId]; } @@ -310,33 +339,37 @@ module.exports = class RolesService { } static async removeRoleInfoById(infoId, roleId, key, currentUser) { + assertWidgetKey(key); + assertUuid(infoId, 'Widget ID'); + const transaction = await db.sequelize.transaction(); - - let role; - if (roleId) { - role = await RolesDBApi.findBy({ id: roleId }, { transaction }); - } else { - role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); - } - if (!role) { - await transaction.rollback(); - throw new ValidationError('rolesNotFound'); - } - - let customization = {}; try { - customization = JSON.parse(role.role_customization || '{}'); - } catch (e) { - console.log(e); - } + let role; + if (roleId) { + role = await RolesDBApi.findBy({ id: roleId }, { transaction }); + } else { + role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); + } + if (!role) { + throw new ValidationError('rolesNotFound'); + } - customization[key] = customization[key].filter( - (item) => item !== infoId, - ); + let customization = {}; + try { + customization = JSON.parse(role.role_customization || '{}'); + } catch (e) { + console.log(e); + } - const response = await axios.delete(`${config.flHost}/${config.project_uuid}/project_customization_widgets/${infoId}.json`); - const { status } = await response; - try { + if (!Array.isArray(customization[key])) { + throw badRequest('Widget customization not found.'); + } + + customization[key] = customization[key].filter( + (item) => item !== infoId, + ); + + await axios.delete(`${config.flHost}/${config.project_uuid}/project_customization_widgets/${infoId}.json`); const result = await RolesDBApi.update( role.id, { @@ -362,14 +395,8 @@ module.exports = class RolesService { static async getRoleInfoByKey(key, roleId, currentUser) { const transaction = await db.sequelize.transaction(); - const organizationId = currentUser.organizationId; - let globalAccess = currentUser.app_role?.globalAccess; - let queryString = ''; - - - try { - const role = await findRole(roleId, currentUser); + const role = await findRole(roleId); const customization = parseCustomization(role); let result; @@ -384,13 +411,8 @@ module.exports = class RolesService { } catch (error) { console.error(error); await transaction.rollback(); - } finally { - if (transaction.finished !== 'commit') { - await transaction.rollback(); - } + throw error; } - - } diff --git a/backend/src/services/schools.js b/backend/src/services/schools.js index 4787552..89dd155 100644 --- a/backend/src/services/schools.js +++ b/backend/src/services/schools.js @@ -1,9 +1,7 @@ const db = require('../db/models'); const SchoolsDBApi = require('../db/api/schools'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const stream = require('stream'); const SCHOOL_STATUSES = new Set(['active', 'setup', 'suspended']); @@ -64,22 +62,7 @@ module.exports = class SchoolsService { const transaction = await db.sequelize.transaction(); try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }) + const results = await parseImportFile(req, res); await SchoolsDBApi.bulkImport(results, { transaction, @@ -100,7 +83,7 @@ module.exports = class SchoolsService { try { let schools = await SchoolsDBApi.findBy( {id}, - {transaction}, + { transaction, currentUser }, ); if (!schools) { diff --git a/backend/src/services/search.js b/backend/src/services/search.js index 751f73a..d22db68 100644 --- a/backend/src/services/search.js +++ b/backend/src/services/search.js @@ -3,6 +3,35 @@ const ValidationError = require('./notifications/errors/validation'); const Sequelize = db.Sequelize; const Op = Sequelize.Op; +const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); + +const MAX_SEARCH_QUERY_LENGTH = 100; +const MAX_RESULTS_PER_TABLE = 10; +const MAX_TOTAL_SEARCH_RESULTS = 100; + +function normalizeSearchQuery(searchQuery) { + if (typeof searchQuery !== 'string') { + throw new ValidationError('iam.errors.searchQueryRequired'); + } + + const normalized = searchQuery.trim(); + if (!normalized) { + throw new ValidationError('iam.errors.searchQueryRequired'); + } + + if (normalized.length > MAX_SEARCH_QUERY_LENGTH) { + const error = new Error(`Search query is too long. Maximum length is ${MAX_SEARCH_QUERY_LENGTH} characters.`); + error.code = 400; + throw error; + } + + return normalized; +} + +function escapeLikePattern(value) { + return value.replace(/[\\%_]/g, '\\$&'); +} + /** * @param {string} permission @@ -14,7 +43,10 @@ async function checkPermissions(permission, currentUser) { throw new ValidationError('auth.unauthorized'); } - const userPermission = currentUser.custom_permissions.find( + const customPermissions = Array.isArray(currentUser.custom_permissions) + ? currentUser.custom_permissions + : []; + const userPermission = customPermissions.find( (cp) => cp.name === permission, ); @@ -22,25 +54,21 @@ async function checkPermissions(permission, currentUser) { return true; } - try { - if (!currentUser.app_role) { - throw new ValidationError('auth.forbidden'); - } - - const permissions = await currentUser.app_role.getPermissions(); - - return !!permissions.find((p) => p.name === permission); - } catch (e) { - throw e; + if (!currentUser.app_role) { + throw new ValidationError('auth.forbidden'); } + + const permissions = await currentUser.app_role.getPermissions(); + + return !!permissions.find((p) => p.name === permission); } module.exports = class SearchService { - static async search(searchQuery, currentUser , organizationId, globalAccess) { - try { - if (!searchQuery) { - throw new ValidationError('iam.errors.searchQueryRequired'); - } + static async search(searchQuery, currentUser, globalAccess) { + const normalizedSearchQuery = normalizeSearchQuery(searchQuery); + const likeSearchQuery = `%${escapeLikePattern(normalizedSearchQuery)}%`; + const lowerSearchQuery = normalizedSearchQuery.toLowerCase(); + const tableColumns = { @@ -427,72 +455,87 @@ module.exports = class SearchService { let allFoundRecords = []; - for (const tableName in tableColumns) { - if (tableColumns.hasOwnProperty(tableName)) { - const attributesToSearch = tableColumns[tableName]; - const attributesIntToSearch = columnsInt[tableName] || []; - const whereCondition = { - [Op.or]: [ - ...attributesToSearch.map(attribute => ({ - [attribute]: { - [Op.iLike] : `%${searchQuery}%`, - }, - })), - ...attributesIntToSearch.map(attribute => ( - Sequelize.where( - Sequelize.cast(Sequelize.col(`${tableName}.${attribute}`), 'varchar'), - { [Op.iLike]: `%${searchQuery}%` } - ) - )), - ], + for (const [tableName, attributesToSearch] of Object.entries(tableColumns)) { + const model = db[tableName]; + if (!model) { + continue; + } + + const attributesIntToSearch = columnsInt[tableName] || []; + const whereCondition = { + [Op.or]: [ + ...attributesToSearch.map(attribute => ({ + [attribute]: { + [Op.iLike]: likeSearchQuery, + }, + })), + ...attributesIntToSearch.map(attribute => ( + Sequelize.where( + Sequelize.cast(Sequelize.col(`${tableName}.${attribute}`), 'varchar'), + { [Op.iLike]: likeSearchQuery } + ) + )), + ], + }; + + if (!globalAccess) { + const schoolId = getCurrentUserSchoolId(currentUser); + const rawAttributes = model.rawAttributes || {}; + + if (!schoolId) { + whereCondition.id = null; + } else if (tableName === 'schools') { + whereCondition.id = schoolId; + } else if (rawAttributes.schoolId) { + whereCondition.schoolId = schoolId; + } else if (rawAttributes.schoolsId) { + whereCondition.schoolsId = schoolId; + } else { + whereCondition.id = null; + } + } + + const hasPermission = await checkPermissions(`READ_${tableName.toUpperCase()}`, currentUser); + if (!hasPermission) { + continue; + } + + const foundRecords = await model.findAll({ + where: whereCondition, + attributes: [...new Set([...attributesToSearch, 'id', ...attributesIntToSearch])], + limit: MAX_RESULTS_PER_TABLE, + }); + + const modifiedRecords = foundRecords.map((record) => { + const matchAttribute = []; + + for (const attribute of attributesToSearch) { + const value = record[attribute]; + if (typeof value === 'string' && value.toLowerCase().includes(lowerSearchQuery)) { + matchAttribute.push(attribute); + } + } + + for (const attribute of attributesIntToSearch) { + const castedValue = String(record[attribute] ?? ''); + if (castedValue.toLowerCase().includes(lowerSearchQuery)) { + matchAttribute.push(attribute); + } + } + + return { + ...record.get(), + matchAttribute, + tableName, }; + }); - - if (!globalAccess && tableName !== 'organizations' && organizationId) { - whereCondition.organizationId = organizationId; - } - - - const hasPermission = await checkPermissions(`READ_${tableName.toUpperCase()}`, currentUser); - if (!hasPermission) { - continue; - } - - const foundRecords = await db[tableName].findAll({ - where: whereCondition, - attributes: [...tableColumns[tableName], 'id', ...attributesIntToSearch], - }); - - const modifiedRecords = foundRecords.map((record) => { - const matchAttribute = []; - - for (const attribute of attributesToSearch) { - if (record[attribute]?.toLowerCase()?.includes(searchQuery.toLowerCase())) { - matchAttribute.push(attribute); - } - } - - for (const attribute of attributesIntToSearch) { - const castedValue = String(record[attribute]); - if (castedValue && castedValue.toLowerCase().includes(searchQuery.toLowerCase())) { - matchAttribute.push(attribute); - } - } - - return { - ...record.get(), - matchAttribute, - tableName, - }; - }); - - allFoundRecords = allFoundRecords.concat(modifiedRecords); + allFoundRecords = allFoundRecords.concat(modifiedRecords); + if (allFoundRecords.length >= MAX_TOTAL_SEARCH_RESULTS) { + break; } } - return allFoundRecords; - } catch (error) { - throw error; - } + return allFoundRecords.slice(0, MAX_TOTAL_SEARCH_RESULTS); } } \ No newline at end of file diff --git a/backend/src/services/sqlSafety.js b/backend/src/services/sqlSafety.js new file mode 100644 index 0000000..527a0e3 --- /dev/null +++ b/backend/src/services/sqlSafety.js @@ -0,0 +1,83 @@ +const db = require('../db/models'); + +const DEFAULT_MAX_ROWS = 500; +const ABSOLUTE_MAX_ROWS = 1000; +const DEFAULT_TIMEOUT_MS = 5000; +const MAX_TIMEOUT_MS = 30000; +const MAX_SQL_LENGTH = 10000; + +function badRequest(message) { + const error = new Error(message); + error.code = 400; + return error; +} + +function normalizePositiveInteger(value, defaultValue, maxValue) { + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed) || parsed <= 0) { + return defaultValue; + } + + return Math.min(parsed, maxValue); +} + +function normalizeSelectSql(sql, options = {}) { + const maxLength = options.maxLength || MAX_SQL_LENGTH; + + if (typeof sql !== 'string' || !sql.trim()) { + throw badRequest('SQL is required'); + } + + const trimmed = sql.trim(); + if (trimmed.length > maxLength) { + throw badRequest(`SQL is too long. Maximum length is ${maxLength} characters.`); + } + + if (trimmed.includes('\0')) { + throw badRequest('SQL contains invalid characters'); + } + + const normalized = trimmed.replace(/;+\s*$/, ''); + + if (!/^select\b/i.test(normalized)) { + throw badRequest('Only SELECT statements are allowed'); + } + + if (normalized.includes(';')) { + throw badRequest('Only a single SELECT statement is allowed'); + } + + return normalized; +} + +function normalizeLimit(limit, defaultLimit = DEFAULT_MAX_ROWS, maxLimit = ABSOLUTE_MAX_ROWS) { + return normalizePositiveInteger(limit, defaultLimit, maxLimit); +} + +function normalizeTimeout(timeoutMs) { + return normalizePositiveInteger(timeoutMs, DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS); +} + +async function executeReadOnlySelect(sql, options = {}) { + const normalized = normalizeSelectSql(sql, options); + const limit = normalizeLimit(options.limit, options.maxRows || DEFAULT_MAX_ROWS, ABSOLUTE_MAX_ROWS); + const timeoutMs = normalizeTimeout(options.timeoutMs); + const limitedSql = `SELECT * FROM (${normalized}) AS "__flatlogic_safe_query" LIMIT ${limit}`; + + return db.sequelize.transaction(async (transaction) => { + await db.sequelize.query('SET TRANSACTION READ ONLY', { transaction }); + await db.sequelize.query(`SET LOCAL statement_timeout = ${timeoutMs}`, { transaction }); + + return db.sequelize.query(limitedSql, { + replacements: options.replacements || {}, + transaction, + type: db.Sequelize.QueryTypes.SELECT, + }); + }); +} + +module.exports = { + executeReadOnlySelect, + normalizeLimit, + normalizeSelectSql, +}; diff --git a/backend/src/services/student_guardians.js b/backend/src/services/student_guardians.js index fd14352..e0fef87 100644 --- a/backend/src/services/student_guardians.js +++ b/backend/src/services/student_guardians.js @@ -1,11 +1,7 @@ const db = require('../db/models'); const Student_guardiansDBApi = require('../db/api/student_guardians'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); @@ -28,28 +24,13 @@ module.exports = class Student_guardiansService { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }) + const results = await parseImportFile(req, res); await Student_guardiansDBApi.bulkImport(results, { transaction, @@ -70,7 +51,7 @@ module.exports = class Student_guardiansService { try { let student_guardians = await Student_guardiansDBApi.findBy( {id}, - {transaction}, + { transaction, currentUser }, ); if (!student_guardians) { @@ -95,7 +76,7 @@ module.exports = class Student_guardiansService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/backend/src/services/students.js b/backend/src/services/students.js index f0d4ac2..d0e193d 100644 --- a/backend/src/services/students.js +++ b/backend/src/services/students.js @@ -1,11 +1,7 @@ const db = require('../db/models'); const StudentsDBApi = require('../db/api/students'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); @@ -28,28 +24,13 @@ module.exports = class StudentsService { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }) + const results = await parseImportFile(req, res); await StudentsDBApi.bulkImport(results, { transaction, @@ -70,7 +51,7 @@ module.exports = class StudentsService { try { let students = await StudentsDBApi.findBy( {id}, - {transaction}, + { transaction, currentUser }, ); if (!students) { @@ -95,7 +76,7 @@ module.exports = class StudentsService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/backend/src/services/subjects.js b/backend/src/services/subjects.js index af47b04..e968b5b 100644 --- a/backend/src/services/subjects.js +++ b/backend/src/services/subjects.js @@ -1,11 +1,7 @@ const db = require('../db/models'); const SubjectsDBApi = require('../db/api/subjects'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); @@ -28,28 +24,13 @@ module.exports = class SubjectsService { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }) + const results = await parseImportFile(req, res); await SubjectsDBApi.bulkImport(results, { transaction, @@ -70,7 +51,7 @@ module.exports = class SubjectsService { try { let subjects = await SubjectsDBApi.findBy( {id}, - {transaction}, + { transaction, currentUser }, ); if (!subjects) { @@ -95,7 +76,7 @@ module.exports = class SubjectsService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/backend/src/services/teachers.js b/backend/src/services/teachers.js index 0482aa8..13d0900 100644 --- a/backend/src/services/teachers.js +++ b/backend/src/services/teachers.js @@ -1,11 +1,7 @@ const db = require('../db/models'); const TeachersDBApi = require('../db/api/teachers'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); @@ -28,28 +24,13 @@ module.exports = class TeachersService { await transaction.rollback(); throw error; } - }; + } - static async bulkImport(req, res, sendInvitationEmails = true, host) { + static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }) + const results = await parseImportFile(req, res); await TeachersDBApi.bulkImport(results, { transaction, @@ -70,7 +51,7 @@ module.exports = class TeachersService { try { let teachers = await TeachersDBApi.findBy( {id}, - {transaction}, + { transaction, currentUser }, ); if (!teachers) { @@ -95,7 +76,7 @@ module.exports = class TeachersService { await transaction.rollback(); throw error; } - }; + } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/backend/src/services/users.js b/backend/src/services/users.js index 41f8220..344f85b 100644 --- a/backend/src/services/users.js +++ b/backend/src/services/users.js @@ -1,15 +1,10 @@ const db = require('../db/models'); const UsersDBApi = require('../db/api/users'); -const processFile = require("../middlewares/upload"); +const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); const config = require('../config'); -const stream = require('stream'); -const InvitationEmail = require('./email/list/invitation'); -const EmailSender = require('./email'); const AuthService = require('./auth'); module.exports = class UsersService { @@ -60,22 +55,7 @@ module.exports = class UsersService { let emailsToInvite = []; try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', () => { - console.log('results csv', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }); + const results = await parseImportFile(req, res); const hasAllEmails = results.every((result) => result.email); @@ -114,7 +94,7 @@ module.exports = class UsersService { try { let users = await UsersDBApi.findBy( {id}, - {transaction}, + { transaction, currentUser }, ); if (!users) { @@ -142,7 +122,7 @@ module.exports = class UsersService { await transaction.rollback(); throw error; } - }; + } static async remove(id, currentUser) { const transaction = await db.sequelize.transaction(); diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index 54b1c07..359f796 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -151,7 +151,7 @@ "addInviteUser": "Add/Invite User", "filter": "Filter", "downloadCsv": "Download CSV", - "uploadCsv": "Upload CSV", + "uploadCsv": "Upload CSV/XLSX", "confirm": "Confirm", "cancel": "Cancel", "delete": "Delete", diff --git a/frontend/public/locales/pt/common.json b/frontend/public/locales/pt/common.json index 74bbdf5..fa5f87f 100644 --- a/frontend/public/locales/pt/common.json +++ b/frontend/public/locales/pt/common.json @@ -151,7 +151,7 @@ "addInviteUser": "Adicionar/convidar utilizador", "filter": "Filtrar", "downloadCsv": "Descarregar CSV", - "uploadCsv": "Carregar CSV", + "uploadCsv": "Carregar CSV/XLSX", "confirm": "Confirmar", "cancel": "Cancelar", "delete": "Eliminar", diff --git a/frontend/src/components/DragDropFilePicker.tsx b/frontend/src/components/DragDropFilePicker.tsx index 1ee20f1..540e8fa 100644 --- a/frontend/src/components/DragDropFilePicker.tsx +++ b/frontend/src/components/DragDropFilePicker.tsx @@ -1,15 +1,22 @@ -import React, { ChangeEvent, useEffect, useState } from 'react'; +import React, { ChangeEvent, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import BaseIcon from './BaseIcon'; import { mdiFileUploadOutline } from '@mdi/js'; +import { dataImportAccept } from '../config'; type Props = { file: File | null; - setFile: (file: File) => void; + setFile: (file: File | null) => void; formats?: string; }; -const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => { +const normalizeFormats = (formats: string) => formats + .split(',') + .map((format) => format.trim().toLowerCase()) + .filter(Boolean) + .map((format) => (format.startsWith('.') ? format : `.${format}`)); + +const DragDropFilePicker = ({ file, setFile, formats = dataImportAccept }: Props) => { const { t } = useTranslation('common'); const [isTranslationMounted, setIsTranslationMounted] = useState(false); const translate = (key: string, fallback: string, options = {}): string => ( @@ -18,25 +25,28 @@ const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => { const [highlight, setHighlight] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const fileInput = React.createRef(); + const effectiveFormats = formats === '.csv' ? dataImportAccept : formats; + const allowedFormats = useMemo(() => normalizeFormats(effectiveFormats), [effectiveFormats]); useEffect(() => { setIsTranslationMounted(true); }, []); useEffect(() => { - if (!file && fileInput) fileInput.current.value = ''; + if (!file && fileInput.current) fileInput.current.value = ''; }, [file, fileInput]); function onFilesAdded(files: FileList | null) { if (files && files[0]) { const newFile = files[0]; - const fileExtension = newFile.name.split('.').pop().toLowerCase(); + const rawExtension = newFile.name.split('.').pop()?.toLowerCase(); + const fileExtension = rawExtension ? `.${rawExtension}` : ''; - if (formats.includes(fileExtension) || !formats) { + if (!allowedFormats.length || allowedFormats.includes(fileExtension)) { setFile(newFile); setErrorMessage(''); } else { - setErrorMessage(translate('common.upload.allowedFormats', `Allowed formats: ${formats}`, { formats })); + setErrorMessage(translate('common.upload.allowedFormats', `Allowed formats: ${effectiveFormats}`, { formats: effectiveFormats })); } } } @@ -109,9 +119,9 @@ const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => {

{translate('common.upload.clickToUpload', 'Click to upload')} {translate('common.upload.dragAndDrop', 'or drag and drop')}

- {formats && ( + {effectiveFormats && (

- {formats} + {effectiveFormats}

)} @@ -121,9 +131,9 @@ const DragDropFilePicker = ({ file, setFile, formats = '' }: Props) => { id='dropzone-file' ref={fileInput} type='file' - accept={formats} + accept={effectiveFormats} className='hidden' - onChange={(e) => onFilesAdded(e.target.files)} + onChange={(e: ChangeEvent) => onFilesAdded(e.target.files)} /> diff --git a/frontend/src/components/FormFilePicker.tsx b/frontend/src/components/FormFilePicker.tsx index 4302edd..8c01b83 100644 --- a/frontend/src/components/FormFilePicker.tsx +++ b/frontend/src/components/FormFilePicker.tsx @@ -4,6 +4,13 @@ import BaseButton from './BaseButton'; import FileUploader from "./Uploaders/UploadService"; import { mdiReload } from '@mdi/js'; import { useAppSelector } from '../stores/hooks'; +import { documentUploadAccept, documentUploadFormatExtensions } from '../config'; + +type FileUploadSchema = { + image?: boolean; + size?: number; + formats?: string[]; +}; type Props = { label?: string; @@ -12,7 +19,7 @@ type Props = { color: ColorButtonKey; isRoundIcon?: boolean; path: string; - schema: object; + schema: FileUploadSchema; field: any; form: any; }; @@ -36,6 +43,14 @@ const FormFilePicker = ({ label, icon, accept, color, isRoundIcon, path, cornersRight = '' } + const allowedFormats = schema?.formats?.length + ? schema.formats + : documentUploadFormatExtensions; + const uploadSchema = { ...schema, formats: allowedFormats }; + const acceptValue = accept || (schema?.formats?.length + ? allowedFormats.map((format) => `.${format}`).join(',') + : documentUploadAccept); + useEffect(() => { if (field.value) { setFile(field.value[0]); @@ -43,11 +58,16 @@ const FormFilePicker = ({ label, icon, accept, color, isRoundIcon, path, }, [field.value]); const handleFileChange = async (event) => { const file = event.currentTarget.files[0]; + + if (!file) { + return; + } + setFile(file); - FileUploader.validate(file, schema); + FileUploader.validate(file, uploadSchema); setLoading(true); - const remoteFile = await FileUploader.upload(path, file, schema); + const remoteFile = await FileUploader.upload(path, file, uploadSchema); form.setFieldValue(field.name, [{ ...remoteFile }]); setLoading(false); @@ -74,7 +94,7 @@ const FormFilePicker = ({ label, icon, accept, color, isRoundIcon, path, type='file' className='absolute top-0 left-0 w-full h-full opacity-0 outline-none cursor-pointer -z-1' onChange={handleFileChange} - accept={accept} + accept={acceptValue} disabled={loading} /> diff --git a/frontend/src/components/Uploaders/UploadService.js b/frontend/src/components/Uploaders/UploadService.js index 33620a6..b6688bc 100644 --- a/frontend/src/components/Uploaders/UploadService.js +++ b/frontend/src/components/Uploaders/UploadService.js @@ -8,7 +8,8 @@ function extractExtensionFrom(filename) { } const regex = /(?:\.([^.]+))?$/; - return regex.exec(filename)[1]; + const match = regex.exec(filename); + return match && match[1] ? match[1].toLowerCase() : null; } export default class FileUploader { @@ -29,8 +30,12 @@ export default class FileUploader { const extension = extractExtensionFrom(file.name); - if (schema.formats && !schema.formats.includes(extension)) { - throw new Error("Invalid format"); + if (schema.formats) { + const allowedFormats = schema.formats.map((format) => String(format).toLowerCase()); + + if (!allowedFormats.includes(extension)) { + throw new Error(`Invalid format. Allowed formats: ${allowedFormats.map((format) => `.${format}`).join(', ')}`); + } } } @@ -63,7 +68,7 @@ export default class FileUploader { formData.append("file", file); formData.append("filename", filename); const uri = `/file/upload/${path}`; - await Axios.post(uri, formData, { + const response = await Axios.post(uri, formData, { headers: { "Content-Type": "multipart/form-data", }, @@ -71,9 +76,6 @@ export default class FileUploader { const privateUrl = `${path}/${filename}`; - console.log("process.env.NODE_ENV in uploadToServer function", process.env.NODE_ENV); - console.log("baseURLApi in uploadToServer function", baseURLApi); - - return `${baseURLApi}/file/download?privateUrl=${privateUrl}`; + return response.data?.url || `${baseURLApi}/file/download?privateUrl=${privateUrl}`; } } diff --git a/frontend/src/config.ts b/frontend/src/config.ts index a9783c8..9c9a687 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -13,3 +13,15 @@ export const appTitle = 'created by Flatlogic generator!' export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle} — ${appTitle}` export const tinyKey = process.env.NEXT_PUBLIC_TINY_KEY || '' +export const documentUploadFormatExtensions = ['doc', 'docx', 'xls', 'xlsx'] + +export const documentUploadAccept = documentUploadFormatExtensions + .map((format) => `.${format}`) + .join(',') + +export const dataImportFormatExtensions = ['csv', 'xlsx'] + +export const dataImportAccept = dataImportFormatExtensions + .map((format) => `.${format}`) + .join(',') + diff --git a/frontend/src/pages/assessments/assessments-list.tsx b/frontend/src/pages/assessments/assessments-list.tsx index 26cf199..598c03e 100644 --- a/frontend/src/pages/assessments/assessments-list.tsx +++ b/frontend/src/pages/assessments/assessments-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableAssessments from '../../components/Assessments/TableAssessments' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -134,7 +134,7 @@ const AssessmentsTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -156,7 +156,7 @@ const AssessmentsTablesPage = () => { { diff --git a/frontend/src/pages/assessments/assessments-table.tsx b/frontend/src/pages/assessments/assessments-table.tsx index fe36a7b..82b57f3 100644 --- a/frontend/src/pages/assessments/assessments-table.tsx +++ b/frontend/src/pages/assessments/assessments-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableAssessments from '../../components/Assessments/TableAssessments' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -121,7 +121,7 @@ const AssessmentsTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -145,7 +145,7 @@ const AssessmentsTablesPage = () => { { diff --git a/frontend/src/pages/attendance/attendance-list.tsx b/frontend/src/pages/attendance/attendance-list.tsx index 4589258..ea1c033 100644 --- a/frontend/src/pages/attendance/attendance-list.tsx +++ b/frontend/src/pages/attendance/attendance-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableAttendance from '../../components/Attendance/TableAttendance' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -126,7 +126,7 @@ const AttendanceTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -148,7 +148,7 @@ const AttendanceTablesPage = () => { { diff --git a/frontend/src/pages/attendance/attendance-table.tsx b/frontend/src/pages/attendance/attendance-table.tsx index 2c95f07..db9417f 100644 --- a/frontend/src/pages/attendance/attendance-table.tsx +++ b/frontend/src/pages/attendance/attendance-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableAttendance from '../../components/Attendance/TableAttendance' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -113,7 +113,7 @@ const AttendanceTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -137,7 +137,7 @@ const AttendanceTablesPage = () => { { diff --git a/frontend/src/pages/book_loans/book_loans-list.tsx b/frontend/src/pages/book_loans/book_loans-list.tsx index 5b3e295..475bee2 100644 --- a/frontend/src/pages/book_loans/book_loans-list.tsx +++ b/frontend/src/pages/book_loans/book_loans-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableBook_loans from '../../components/Book_loans/TableBook_loans' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -130,7 +130,7 @@ const Book_loansTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -156,7 +156,7 @@ const Book_loansTablesPage = () => { { diff --git a/frontend/src/pages/book_loans/book_loans-table.tsx b/frontend/src/pages/book_loans/book_loans-table.tsx index b7bd3e6..028b1d2 100644 --- a/frontend/src/pages/book_loans/book_loans-table.tsx +++ b/frontend/src/pages/book_loans/book_loans-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableBook_loans from '../../components/Book_loans/TableBook_loans' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -117,7 +117,7 @@ const Book_loansTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -141,7 +141,7 @@ const Book_loansTablesPage = () => { { diff --git a/frontend/src/pages/books/books-list.tsx b/frontend/src/pages/books/books-list.tsx index 7b40c11..3d5ec48 100644 --- a/frontend/src/pages/books/books-list.tsx +++ b/frontend/src/pages/books/books-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableBooks from '../../components/Books/TableBooks' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -122,7 +122,7 @@ const BooksTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -144,7 +144,7 @@ const BooksTablesPage = () => { { diff --git a/frontend/src/pages/books/books-table.tsx b/frontend/src/pages/books/books-table.tsx index 6faf480..4333e05 100644 --- a/frontend/src/pages/books/books-table.tsx +++ b/frontend/src/pages/books/books-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableBooks from '../../components/Books/TableBooks' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -109,7 +109,7 @@ const BooksTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -133,7 +133,7 @@ const BooksTablesPage = () => { { diff --git a/frontend/src/pages/classes/classes-list.tsx b/frontend/src/pages/classes/classes-list.tsx index c8dd9ff..dbcfe18 100644 --- a/frontend/src/pages/classes/classes-list.tsx +++ b/frontend/src/pages/classes/classes-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableClasses from '../../components/Classes/TableClasses' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -126,7 +126,7 @@ const ClassesTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -148,7 +148,7 @@ const ClassesTablesPage = () => { { diff --git a/frontend/src/pages/classes/classes-table.tsx b/frontend/src/pages/classes/classes-table.tsx index 211547d..31d9b17 100644 --- a/frontend/src/pages/classes/classes-table.tsx +++ b/frontend/src/pages/classes/classes-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableClasses from '../../components/Classes/TableClasses' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -113,7 +113,7 @@ const ClassesTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -137,7 +137,7 @@ const ClassesTablesPage = () => { { diff --git a/frontend/src/pages/courses/courses-list.tsx b/frontend/src/pages/courses/courses-list.tsx index 9265266..206f416 100644 --- a/frontend/src/pages/courses/courses-list.tsx +++ b/frontend/src/pages/courses/courses-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableCourses from '../../components/Courses/TableCourses' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -122,7 +122,7 @@ const CoursesTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -144,7 +144,7 @@ const CoursesTablesPage = () => { { diff --git a/frontend/src/pages/courses/courses-table.tsx b/frontend/src/pages/courses/courses-table.tsx index 3b7c524..49981cb 100644 --- a/frontend/src/pages/courses/courses-table.tsx +++ b/frontend/src/pages/courses/courses-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableCourses from '../../components/Courses/TableCourses' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -109,7 +109,7 @@ const CoursesTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -133,7 +133,7 @@ const CoursesTablesPage = () => { { diff --git a/frontend/src/pages/employees/employees-list.tsx b/frontend/src/pages/employees/employees-list.tsx index 438488f..003933e 100644 --- a/frontend/src/pages/employees/employees-list.tsx +++ b/frontend/src/pages/employees/employees-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableEmployees from '../../components/Employees/TableEmployees' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -122,7 +122,7 @@ const EmployeesTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -144,7 +144,7 @@ const EmployeesTablesPage = () => { { diff --git a/frontend/src/pages/employees/employees-table.tsx b/frontend/src/pages/employees/employees-table.tsx index f3ffee5..2192619 100644 --- a/frontend/src/pages/employees/employees-table.tsx +++ b/frontend/src/pages/employees/employees-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableEmployees from '../../components/Employees/TableEmployees' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -109,7 +109,7 @@ const EmployeesTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -133,7 +133,7 @@ const EmployeesTablesPage = () => { { diff --git a/frontend/src/pages/enrollments/enrollments-list.tsx b/frontend/src/pages/enrollments/enrollments-list.tsx index a29cdb8..991e712 100644 --- a/frontend/src/pages/enrollments/enrollments-list.tsx +++ b/frontend/src/pages/enrollments/enrollments-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableEnrollments from '../../components/Enrollments/TableEnrollments' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -130,7 +130,7 @@ const EnrollmentsTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -152,7 +152,7 @@ const EnrollmentsTablesPage = () => { { diff --git a/frontend/src/pages/enrollments/enrollments-table.tsx b/frontend/src/pages/enrollments/enrollments-table.tsx index 443af92..a500df2 100644 --- a/frontend/src/pages/enrollments/enrollments-table.tsx +++ b/frontend/src/pages/enrollments/enrollments-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableEnrollments from '../../components/Enrollments/TableEnrollments' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -117,7 +117,7 @@ const EnrollmentsTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -141,7 +141,7 @@ const EnrollmentsTablesPage = () => { { diff --git a/frontend/src/pages/grades/grades-list.tsx b/frontend/src/pages/grades/grades-list.tsx index 32f9a26..26b3b83 100644 --- a/frontend/src/pages/grades/grades-list.tsx +++ b/frontend/src/pages/grades/grades-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableGrades from '../../components/Grades/TableGrades' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -122,7 +122,7 @@ const GradesTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -148,7 +148,7 @@ const GradesTablesPage = () => { { diff --git a/frontend/src/pages/grades/grades-table.tsx b/frontend/src/pages/grades/grades-table.tsx index c78d10f..640d672 100644 --- a/frontend/src/pages/grades/grades-table.tsx +++ b/frontend/src/pages/grades/grades-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableGrades from '../../components/Grades/TableGrades' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -109,7 +109,7 @@ const GradesTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -133,7 +133,7 @@ const GradesTablesPage = () => { { diff --git a/frontend/src/pages/guardians/guardians-list.tsx b/frontend/src/pages/guardians/guardians-list.tsx index d2a25f0..998cc2f 100644 --- a/frontend/src/pages/guardians/guardians-list.tsx +++ b/frontend/src/pages/guardians/guardians-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableGuardians from '../../components/Guardians/TableGuardians' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -122,7 +122,7 @@ const GuardiansTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -144,7 +144,7 @@ const GuardiansTablesPage = () => { { diff --git a/frontend/src/pages/guardians/guardians-table.tsx b/frontend/src/pages/guardians/guardians-table.tsx index 3d1ee89..9207bd7 100644 --- a/frontend/src/pages/guardians/guardians-table.tsx +++ b/frontend/src/pages/guardians/guardians-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableGuardians from '../../components/Guardians/TableGuardians' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -109,7 +109,7 @@ const GuardiansTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -133,7 +133,7 @@ const GuardiansTablesPage = () => { { diff --git a/frontend/src/pages/invoices/invoices-list.tsx b/frontend/src/pages/invoices/invoices-list.tsx index 5136fca..7471de9 100644 --- a/frontend/src/pages/invoices/invoices-list.tsx +++ b/frontend/src/pages/invoices/invoices-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableInvoices from '../../components/Invoices/TableInvoices' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -126,7 +126,7 @@ const InvoicesTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -150,7 +150,7 @@ const InvoicesTablesPage = () => { { diff --git a/frontend/src/pages/invoices/invoices-table.tsx b/frontend/src/pages/invoices/invoices-table.tsx index a448dae..94d668a 100644 --- a/frontend/src/pages/invoices/invoices-table.tsx +++ b/frontend/src/pages/invoices/invoices-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableInvoices from '../../components/Invoices/TableInvoices' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -113,7 +113,7 @@ const InvoicesTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -137,7 +137,7 @@ const InvoicesTablesPage = () => { { diff --git a/frontend/src/pages/payments/payments-list.tsx b/frontend/src/pages/payments/payments-list.tsx index d8bcf20..8c2846b 100644 --- a/frontend/src/pages/payments/payments-list.tsx +++ b/frontend/src/pages/payments/payments-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TablePayments from '../../components/Payments/TablePayments' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -126,7 +126,7 @@ const PaymentsTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -148,7 +148,7 @@ const PaymentsTablesPage = () => { { diff --git a/frontend/src/pages/payments/payments-table.tsx b/frontend/src/pages/payments/payments-table.tsx index 7d8be9c..7463964 100644 --- a/frontend/src/pages/payments/payments-table.tsx +++ b/frontend/src/pages/payments/payments-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TablePayments from '../../components/Payments/TablePayments' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -113,7 +113,7 @@ const PaymentsTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -137,7 +137,7 @@ const PaymentsTablesPage = () => { { diff --git a/frontend/src/pages/permissions/permissions-list.tsx b/frontend/src/pages/permissions/permissions-list.tsx index 050f3cb..f9bc62b 100644 --- a/frontend/src/pages/permissions/permissions-list.tsx +++ b/frontend/src/pages/permissions/permissions-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TablePermissions from '../../components/Permissions/TablePermissions' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -120,7 +120,7 @@ const PermissionsTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -142,7 +142,7 @@ const PermissionsTablesPage = () => { { diff --git a/frontend/src/pages/permissions/permissions-table.tsx b/frontend/src/pages/permissions/permissions-table.tsx index 88694db..727a920 100644 --- a/frontend/src/pages/permissions/permissions-table.tsx +++ b/frontend/src/pages/permissions/permissions-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TablePermissions from '../../components/Permissions/TablePermissions' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -107,7 +107,7 @@ const PermissionsTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -127,7 +127,7 @@ const PermissionsTablesPage = () => { { diff --git a/frontend/src/pages/products/products-list.tsx b/frontend/src/pages/products/products-list.tsx index dff74d6..d12d003 100644 --- a/frontend/src/pages/products/products-list.tsx +++ b/frontend/src/pages/products/products-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableProducts from '../../components/Products/TableProducts' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -122,7 +122,7 @@ const ProductsTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -144,7 +144,7 @@ const ProductsTablesPage = () => { { diff --git a/frontend/src/pages/products/products-table.tsx b/frontend/src/pages/products/products-table.tsx index df9d27f..755e998 100644 --- a/frontend/src/pages/products/products-table.tsx +++ b/frontend/src/pages/products/products-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableProducts from '../../components/Products/TableProducts' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -109,7 +109,7 @@ const ProductsTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -133,7 +133,7 @@ const ProductsTablesPage = () => { { diff --git a/frontend/src/pages/profile.tsx b/frontend/src/pages/profile.tsx index f5eb7cf..7de8380 100644 --- a/frontend/src/pages/profile.tsx +++ b/frontend/src/pages/profile.tsx @@ -4,9 +4,8 @@ import { } from '@mdi/js'; import Head from 'next/head'; import React, { ReactElement, useEffect, useState } from 'react'; -import { ToastContainer, toast } from 'react-toastify'; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; +import { toast } from 'react-toastify'; +import axios from 'axios'; import CardBox from '../components/CardBox'; import LayoutAuthenticated from '../layouts/Authenticated'; @@ -19,19 +18,13 @@ import FormField from '../components/FormField'; import BaseDivider from '../components/BaseDivider'; import BaseButtons from '../components/BaseButtons'; import BaseButton from '../components/BaseButton'; -import FormCheckRadio from '../components/FormCheckRadio'; -import FormCheckRadioGroup from '../components/FormCheckRadioGroup'; import FormImagePicker from '../components/FormImagePicker'; -import { SwitchField } from '../components/SwitchField'; -import { SelectField } from '../components/SelectField'; - -import { update, fetch } from '../stores/users/usersSlice'; import { useAppDispatch, useAppSelector } from '../stores/hooks'; import { useRouter } from 'next/router'; import {findMe} from "../stores/authSlice"; const EditUsers = () => { - const { currentUser, isFetching, token } = useAppSelector( + const { currentUser } = useAppSelector( (state) => state.auth, ); const router = useRouter(); @@ -42,8 +35,6 @@ const EditUsers = () => { lastName: '', phoneNumber: '', email: '', - app_role: '', - disabled: false, avatar: [], password: '' }; @@ -56,15 +47,23 @@ const EditUsers = () => { Object.keys(initVals).forEach( (el) => (newInitialVal[el] = currentUser[el]), ); + newInitialVal.password = ''; setInitialValues(newInitialVal); } }, [currentUser]); const handleSubmit = async (data) => { - await dispatch(update({ id: currentUser.id, data })); + const profile = { + firstName: data.firstName, + lastName: data.lastName, + phoneNumber: data.phoneNumber, + password: data.password, + avatar: data.avatar, + }; + + await axios.put('auth/profile', { profile }); await dispatch(findMe()); - await router.push('/users/users-list'); notify('success', 'Profile was updated!'); }; @@ -124,31 +123,13 @@ const EditUsers = () => { - - - - - - - - @@ -162,7 +143,7 @@ const EditUsers = () => { color='danger' outline label='Cancel' - onClick={() => router.push('/users/users-list')} + onClick={() => router.push('/')} /> diff --git a/frontend/src/pages/roles/roles-list.tsx b/frontend/src/pages/roles/roles-list.tsx index dc3ad35..b5678de 100644 --- a/frontend/src/pages/roles/roles-list.tsx +++ b/frontend/src/pages/roles/roles-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableRoles from '../../components/Roles/TableRoles' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -120,7 +120,7 @@ const RolesTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -142,7 +142,7 @@ const RolesTablesPage = () => { { diff --git a/frontend/src/pages/roles/roles-table.tsx b/frontend/src/pages/roles/roles-table.tsx index ac487a8..a5c4819 100644 --- a/frontend/src/pages/roles/roles-table.tsx +++ b/frontend/src/pages/roles/roles-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableRoles from '../../components/Roles/TableRoles' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -107,7 +107,7 @@ const RolesTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -127,7 +127,7 @@ const RolesTablesPage = () => { { diff --git a/frontend/src/pages/schools/schools-list.tsx b/frontend/src/pages/schools/schools-list.tsx index 115b2aa..c00884a 100644 --- a/frontend/src/pages/schools/schools-list.tsx +++ b/frontend/src/pages/schools/schools-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableSchools from '../../components/Schools/TableSchools' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -120,7 +120,7 @@ const SchoolsTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -142,7 +142,7 @@ const SchoolsTablesPage = () => { { diff --git a/frontend/src/pages/schools/schools-table.tsx b/frontend/src/pages/schools/schools-table.tsx index 2a2aa49..28131e5 100644 --- a/frontend/src/pages/schools/schools-table.tsx +++ b/frontend/src/pages/schools/schools-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableSchools from '../../components/Schools/TableSchools' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -107,7 +107,7 @@ const SchoolsTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -127,7 +127,7 @@ const SchoolsTablesPage = () => { { diff --git a/frontend/src/pages/student_guardians/student_guardians-list.tsx b/frontend/src/pages/student_guardians/student_guardians-list.tsx index 6079618..8cdbf86 100644 --- a/frontend/src/pages/student_guardians/student_guardians-list.tsx +++ b/frontend/src/pages/student_guardians/student_guardians-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableStudent_guardians from '../../components/Student_guardians/TableStudent_guardians' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -128,7 +128,7 @@ const Student_guardiansTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -150,7 +150,7 @@ const Student_guardiansTablesPage = () => { { diff --git a/frontend/src/pages/student_guardians/student_guardians-table.tsx b/frontend/src/pages/student_guardians/student_guardians-table.tsx index 27f1816..91e4611 100644 --- a/frontend/src/pages/student_guardians/student_guardians-table.tsx +++ b/frontend/src/pages/student_guardians/student_guardians-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableStudent_guardians from '../../components/Student_guardians/TableStudent_guardians' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -115,7 +115,7 @@ const Student_guardiansTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -139,7 +139,7 @@ const Student_guardiansTablesPage = () => { { diff --git a/frontend/src/pages/students/students-list.tsx b/frontend/src/pages/students/students-list.tsx index 1c23274..b911e12 100644 --- a/frontend/src/pages/students/students-list.tsx +++ b/frontend/src/pages/students/students-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableStudents from '../../components/Students/TableStudents' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -122,7 +122,7 @@ const StudentsTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -144,7 +144,7 @@ const StudentsTablesPage = () => { { diff --git a/frontend/src/pages/students/students-table.tsx b/frontend/src/pages/students/students-table.tsx index 7df0fc8..9fd1e12 100644 --- a/frontend/src/pages/students/students-table.tsx +++ b/frontend/src/pages/students/students-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableStudents from '../../components/Students/TableStudents' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -109,7 +109,7 @@ const StudentsTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -133,7 +133,7 @@ const StudentsTablesPage = () => { { diff --git a/frontend/src/pages/subjects/subjects-list.tsx b/frontend/src/pages/subjects/subjects-list.tsx index 6da6669..70b5fc3 100644 --- a/frontend/src/pages/subjects/subjects-list.tsx +++ b/frontend/src/pages/subjects/subjects-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableSubjects from '../../components/Subjects/TableSubjects' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -122,7 +122,7 @@ const SubjectsTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -148,7 +148,7 @@ const SubjectsTablesPage = () => { { diff --git a/frontend/src/pages/subjects/subjects-table.tsx b/frontend/src/pages/subjects/subjects-table.tsx index 57be599..a70b298 100644 --- a/frontend/src/pages/subjects/subjects-table.tsx +++ b/frontend/src/pages/subjects/subjects-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableSubjects from '../../components/Subjects/TableSubjects' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -109,7 +109,7 @@ const SubjectsTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -133,7 +133,7 @@ const SubjectsTablesPage = () => { { diff --git a/frontend/src/pages/teachers/teachers-list.tsx b/frontend/src/pages/teachers/teachers-list.tsx index 522b4b0..59e436b 100644 --- a/frontend/src/pages/teachers/teachers-list.tsx +++ b/frontend/src/pages/teachers/teachers-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableTeachers from '../../components/Teachers/TableTeachers' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -122,7 +122,7 @@ const TeachersTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -144,7 +144,7 @@ const TeachersTablesPage = () => { { diff --git a/frontend/src/pages/teachers/teachers-table.tsx b/frontend/src/pages/teachers/teachers-table.tsx index 1dddca2..be927ed 100644 --- a/frontend/src/pages/teachers/teachers-table.tsx +++ b/frontend/src/pages/teachers/teachers-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableTeachers from '../../components/Teachers/TableTeachers' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -109,7 +109,7 @@ const TeachersTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -133,7 +133,7 @@ const TeachersTablesPage = () => { { diff --git a/frontend/src/pages/users/users-list.tsx b/frontend/src/pages/users/users-list.tsx index 066323f..8dcac70 100644 --- a/frontend/src/pages/users/users-list.tsx +++ b/frontend/src/pages/users/users-list.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableUsers from '../../components/Users/TableUsers' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -126,7 +126,7 @@ const UsersTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -148,7 +148,7 @@ const UsersTablesPage = () => { { diff --git a/frontend/src/pages/users/users-table.tsx b/frontend/src/pages/users/users-table.tsx index 88caea1..ff62291 100644 --- a/frontend/src/pages/users/users-table.tsx +++ b/frontend/src/pages/users/users-table.tsx @@ -6,7 +6,7 @@ import CardBox from '../../components/CardBox' import LayoutAuthenticated from '../../layouts/Authenticated' import SectionMain from '../../components/SectionMain' import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton' -import { getPageTitle } from '../../config' +import { dataImportAccept, getPageTitle } from '../../config' import TableUsers from '../../components/Users/TableUsers' import BaseButton from '../../components/BaseButton' import axios from "axios"; @@ -113,7 +113,7 @@ const UsersTablesPage = () => { {hasCreatePermission && ( setIsModalActive(true)} /> )} @@ -133,7 +133,7 @@ const UsersTablesPage = () => { {