feat: Adding privacy to loads and companies endpoint

This commit is contained in:
Josepablo Cruz
2026-03-30 19:08:36 -06:00
parent e2582f7464
commit 30cd506862
8 changed files with 270 additions and 40 deletions

View File

@@ -7,6 +7,11 @@ ETA Viaporte API
- NodeJS v18
- Docker
# Audience
This document is oriented to front-end developers integrating with the ETA API.
Use the Quick Start section first, then go to the endpoint section you need.
# Endpoints
All endpoints that return a list of elements is paginable with the following queries:
@@ -18,6 +23,71 @@ Example:
- `/endpoint?page=2` : Get page 2 with default (10) elements per page.
- `/endpoint?elements=50` : Get page 0 with 50 elements.
## Quick Start For Front-End Integration
### Authentication
Private endpoints require JWT authorization.
1. Login with `POST /account/authorize`.
2. Store `accessToken` and `session_token`.
3. Send JWT in request headers:
```http
Authorization: Bearer <accessToken>
```
4. When JWT expires, request a new token with `GET /account/authorize/:session_token`.
### Account Lifecycle (Common Front-End Flow)
1. Signup request: `POST /account/signup` returns `checksum`.
2. Signup confirmation: `PATCH /account/signup` with `otp` + `checksum`.
3. Login: `POST /account/authorize`.
4. Register company profile: `POST /account/register`.
### Pagination
Pagination is available on list endpoints using:
- `elements`: page size
- `page`: page index (starts at 0)
Examples:
- `/loads?elements=20&page=0`
- `/loads?page=1`
- `/loads?elements=100`
### Filters And Sorting
Many `find` endpoints support dynamic filters and Mongo-style operators.
Common patterns:
- `field[$regex]=text`
- `field[$options]=i`
- `date[gte]=2026-01-01`
- `date[lte]=2026-01-31`
- `privacy=true`
- `$sort[createdAt]=-1`
### Quick Endpoint Map
Public resources:
- Account and auth: `/account`
- Catalogs: `/countries`, `/cities`, `/states`, `/meta-data`, `/meta-groups`, `/product-categories`
- Public business data: `/public-companies`, `/public-loads`, `/public-vehicles`
- Contact: `/contact-email`
Private resources:
- Company and members: `/companies`, `/users`
- Operations: `/loads`, `/proposals`, `/vehicles`, `/branches`, `/budgets`
- Files and alerts: `/load-attachments`, `/notifications`
- Company private list: `/groups`
## Public endpoints
Read registered resources:
@@ -39,19 +109,19 @@ All these endpoints support the following parameters (except for those with `pub
- `/` : List registered resources with pagination.
- `/:id` : Read specific resource identified by Id.
- `/find?regex=xxx` : List resources that matches with regex (support pagination).
- `/find?regex=xxx` : List resources that match a regex (supports pagination).
### /account
This endpoint provides mechanisms to register, login, recover password and renew JWT.
The __Login__ and __Renew__ process will return 2 tokens, the `accessToken` (JWT) which is used in every further request to authorize access to private endpoints. And the 2nd is the `session_token` (renew token), which should be used after the expiration of the JWT, in order to generate another _JWT_ and _session token_ without the need of use the _email_ and _password__.
The __Login__ and __Renew__ processes return 2 tokens: `accessToken` (JWT), used for private endpoint authorization, and `session_token` (renew token), used after JWT expiration to generate a new JWT and session token without sending email and password again.
The _session token_ expiration is typically 30 days after its generation. Every renewal replaces the token in the DB and the expiration is reset again to 30 days.
#### POST /account/authorize
Login process, returns a JWT and Renew Token
Login process, returns a JWT and renew token
Expects a body with the following data:
```{.json}
@@ -94,7 +164,7 @@ Returns:
#### POST /account/signup
Create a new user. This will trigger an email with the OTP (one time password) to verify the email. There is no expiration time, but it is expected that the Fron End removes the checksum from the local storage after an expiration time defined in the Front End.
Create a new user. This triggers an email with an OTP (one-time password) to verify the email. There is no expiration time, but it is expected that the Front End removes the checksum from local storage after a client-defined timeout.
This will return a checksum string to be used in the confirmation process.
@@ -116,7 +186,7 @@ Returns:
#### PATCH /account/signup
Confirms registration of new user. This will trigger a welcome email to the user.
There is no timeout to confirm the email, but it is expected that the Fron End removes the checksum from the local storage after an expiration time defined in the Front End.
There is no timeout to confirm the email, but it is expected that the Front End removes the checksum from local storage after a client-defined timeout.
If the checksum matches but the user is already registered, then this request will be rejected.
@@ -140,7 +210,7 @@ Returns:
#### POST /account/recover
Reset password request. This will trigger an email with the OTP (one time password) to verify the email. There is no expiration time, but it is expected that the Fron End removes the checksum from the local storage after an expiration time defined in the Front End.
Reset password request. This triggers an email with an OTP (one-time password) to verify the email. There is no expiration time, but it is expected that the Front End removes the checksum from local storage after a client-defined timeout.
This will return a checksum string to be used in the confirmation process.
@@ -162,7 +232,7 @@ Returns:
#### PATCH /account/recover
Confirms the email to recover the password.
There is no timeout to confirm the email, but it is expected that the Fron End removes the checksum from the local storage after an expiration time defined in the Front End.
There is no timeout to confirm the email, but it is expected that the Front End removes the checksum from local storage after a client-defined timeout.
Expects a body with the same data as the POST request, but adding the OTP received in the email, and the checksum generated by the POST request. Here is an example:
@@ -206,6 +276,7 @@ Returns:
### GET /public-companies
Get public fields from registered companies.
This endpoint only returns non-private companies (`privacy=false` or missing).
- `GET /shipper`: List registered shippers that are not hidden only.
- `GET /carrier`: List registered carriers that are not hidden only.
@@ -213,6 +284,7 @@ Get public fields from registered companies.
### GET /public-loads
Get public fields from registered loads.
This endpoint only returns published, non-private loads (`privacy=false` or missing).
- `GET /`: List only loads with status Published.
@@ -239,6 +311,12 @@ The following list of endpoints requires a JWT.
- `GET /load-attachments`: List load attachments related to my company or load id.
- `GET /groups/private`: Get the private group for my company.
Recommended header for all private endpoint calls:
```http
Authorization: Bearer <accessToken>
```
### /public-load-tracking
- `GET /:id`: Get tracking data from load id
@@ -344,10 +422,12 @@ This endpoint is part of /loads endpoint
- `GET /own` : Get own company data.
- `PATCH /own` : Update own company data.
- `GET /shipper` : Get list of shipper companies with pagination using the following filters: company_type, company_name, truck_type, categories, company_state, company_city.
- `GET /shipper` : Get list of shipper companies with pagination using the following filters: company_type, company_name, truck_type, categories, company_state, company_city, privacy.
- $sort[ field ] : -1/1 ; Sort result by field name
- `GET /carrier` : Get list of carrier companies with pagination using the following filters: company_type, company_name, truck_type, categories, company_state, company_city
- `GET /carrier` : Get list of carrier companies with pagination using the following filters: company_type, company_name, truck_type, categories, company_state, company_city, privacy.
- $sort[ field ] : -1/1 ; Sort result by field name
- `privacy=true`: return only private companies that explicitly granted visibility to my company through groups.
- `privacy=false` or unset: return only non-private companies.
- `GET /users/:companyId` : Get the list of users within a company.
- `GET /:id` : Get data from specific company.
@@ -360,7 +440,7 @@ This endpoint is part of /loads endpoint
### /load-attachments
- `POST /loading/:id` : Upload/Update a loading attachment.
- `POST /downloading/:id` : Upload/Update a download attachment
- `POST /downloading/:id` : Upload/Update an unloading attachment.
- `GET /load/:id` : Get the list of attachment ids from load.
- `GET /:id` : Get attachment file.
- `GET /` : Get attachment list from company.
@@ -388,10 +468,12 @@ This endpoint is part of /loads endpoint
- est_unloading_date[lte] : Date less than.
- company_name[$regex] : Regex string to find company_name
- company_name[$options] : Regex options from MongoDB filter description
- privacy : `true` to list private loads shared with my company through groups; `false` or unset to list non-private loads.
- $sort[ field ] : -1/1 ; Sort result by field name
- alert_list: List of emails to notify whenever there is a change to the object.
- `GET /driver` : Find a list of loads for driver views with the same filter and pagination behavior used in `/find`. Doesn't take privacy into account (already handled by driver assignation).
- `GET /calendar` : Find a list of elements with any of the following fields:
- date[gte] : Date grater than.
- date[gte] : Date greater than.
- date[lte] : Date less than.
- load_status : string enumerator ['Published', 'Loading', 'Transit', 'Downloading', 'Delivered'].
- global : 1 To return calendar of all company, or 0/undefined for personal calendar only.
@@ -451,7 +533,7 @@ Work In Progress
__This endpoint is only valid for carriers.__
- `GET /find` : Find a list of elements (from the company it self only) with any of the following fields:
- `GET /find` : Find a list of elements (from the company itself only) with any of the following fields:
- is_available,
- categories,
- active_load,
@@ -497,7 +579,7 @@ __This endpoint is only valid for carriers.__
### Public
The following endpoints doesn't require a JWT authorization
The following endpoints do not require JWT authorization.
#### observers/account/

View File

@@ -1,11 +1,12 @@
"use strict";
const { ROOT_PATH, MODELS_PATH, HANDLERS_PATH, LIB_PATH } = process.env;
const { getModel } = require( `${ROOT_PATH}/${MODELS_PATH}` );
const { getModel } = require( '../../../lib/Models' );
const { GenericHandler } = require( '../../../lib/Handlers/Generic.handler.js' );
const { getPagination } = require( `${ROOT_PATH}/${LIB_PATH}/Misc.js` );
const { getPagination } = require( '../../../lib/Misc.js' );
const usersModel = getModel('users');
const companiesModel = getModel('companies');
const companyGroupsModel = getModel('company_groups');
const branchesModel = getModel('branches');
const vehiclesModel = getModel('vehicles');
const loadsModel = getModel('loads');
@@ -46,21 +47,60 @@ function getAndFilterList( query ){
return filter_list;
}
async function getListByType( type , req ){
const filter = { "company_type" : type , "is_hidden" : false };
const select = [
"rfc",
"company_name",
"company_type",
"company_code",
"company_city",
"company_state",
"createdAt",
"membership",
"categories",
"truck_type",
"company_description"
];
const companySelectField =
[
"rfc",
"company_name",
"company_type",
"company_code",
"company_city",
"company_state",
"createdAt",
"membership",
"categories",
"truck_type",
"company_description",
"privacy"
];
async function getCompanyIdListFromGroups( companyId ){
const privateGroups = await companyGroupsModel.find({
allowedCompanies: { $in: [companyId] }
});
if( !privateGroups ){
return null;
}
const companiesIds = privateGroups.map((group) => group.owner);
return companiesIds;
}
async function getListByType( companyId, type , req ){
const { privacy } = req.query;
const privacyVal = ( privacy && ( privacy >= 1 || privacy.toLowerCase() === 'true' ))? true: false;
let filter;
if( privacyVal ){
const companiesIds = await getCompanyIdListFromGroups( companyId ) || [];
filter = {
_id : { $in : companiesIds },
company_type: type,
is_hidden: false,
privacy: true,
}
}else{
filter = {
company_type: type,
is_hidden: false,
$or : [
{ privacy : false },
{ privacy : { $exists : false } }
]
};
}
const select = companySelectField;
const { $sort } = req.query;
const { elements , page } = getPagination( req.query );
let query_elements;
@@ -143,7 +183,8 @@ async function getCompanyById( req , res ) {
async function getListShippers( req , res ) {
try{
const retVal = await getListByType( "Shipper" , req );
const companyId = req.context.companyId;
const retVal = await getListByType( companyId, "Shipper" , req );
res.send( retVal );
}catch( error ){
console.error( error );
@@ -153,7 +194,8 @@ async function getListShippers( req , res ) {
async function getListCarriers( req , res ) {
try{
const retVal = await getListByType( "Carrier" , req );
const companyId = req.context.companyId;
const retVal = await getListByType( companyId, "Carrier" , req );
res.send( retVal );
}catch( error ){
console.error( error );

View File

@@ -3,6 +3,7 @@ const router = require('express').Router();
const services= require('./services.js');
router.get('/find', services.findList);
router.get('/driver', services.findDriverList);
router.get('/calendar', services.findCalendarList);
router.post('/new', services.postLoad);

View File

@@ -7,6 +7,7 @@ const Model = getModel('loads');
const CompanyModel = getModel('companies');
const ProposalsModel = getModel('proposals');
const branchesModel = getModel('branches');
const companyGroupsModel = getModel('company_groups');
const carrier_projection = [
'company_name',
@@ -141,7 +142,21 @@ function getAndFilterList( query ){
return filter_list;
}
async function findLoads( query ){
async function getCompanyIdListFromGroups( companyId ){
const privateGroups = await companyGroupsModel.find({
allowedCompanies: { $in: [companyId] }
});
if( !privateGroups ){
return null;
}
const companiesIds = privateGroups.map((group) => group.owner);
return companiesIds;
}
async function findDriverLoads( query ){
const { $sort, company_name } = query;
const { page, elements } = getPagination( query );
const andFilterList = getAndFilterList( query ) || [];
@@ -184,6 +199,65 @@ async function findLoads( query ){
};
}
async function findLoads( companyId, query ){
const { $sort, company_name } = query;
const { page, elements } = getPagination( query );
const andFilterList = getAndFilterList( query ) || [];
const { privacy } = query;
const privacyVal = ( privacy && ( privacy >= 1 || privacy.toLowerCase() === 'true' ))? true: false;
let filter;
if( privacyVal ){
const companiesIds = await getCompanyIdListFromGroups( companyId ) || [];
filter = {
company : { $in : companiesIds },
privacy: true,
}
}else{
filter = {
$or : [
{ privacy : false },
{ privacy : { $exists : false } }
]
}
}
if( company_name ){
/* Populate list of company ids with match on the company_name */
const company_list = await CompanyModel.find( { company_name }, [ "id" ] );
const or_company_list = []
company_list.forEach( (item) =>{
or_company_list.push({"company" : item.id});
})
andFilterList.push({
$or : or_company_list
});
}
if( andFilterList.length > 0 ){
filter.$and = andFilterList;
}
const { total , limit, skip, data } = await generic.getList( page , elements, filter, null, $sort );
const load_list = data;
for(let i=0; i<load_list.length; i++){
const load_id = load_list[ i ].id;
load_list[i] = load_list[i].toObject();
const no_of_proposals = await ProposalsModel.count({ load : load_id });
load_list[i].no_of_proposals = no_of_proposals;
}
return {
total,
limit,
skip,
data : load_list
};
}
/**
* Busqueda de cargas por fecha, asumiendo que la companyId es
* siempre la del usuario solicitando los datos.
@@ -281,8 +355,9 @@ const findCalendarList = async(req, res) => {
const findList = async(req, res) => {
try{
const companyId = req.context.companyId;
const query = req.query || {};
const retVal = await findLoads( query );
const retVal = await findLoads( companyId, query );
res.send( retVal );
}catch(error){
console.error( error );
@@ -290,6 +365,17 @@ const findList = async(req, res) => {
}
};
const findDriverList = async ( req, res ) => {
try{
const query = req.query || {};
const retVal = await findDriverLoads( companyId, query );
res.send( retVal );
}catch(error){
console.error( error );
return res.status( 500 ).send({ error });
}
}
const getById = async(req, res) => {
try{
const elementId = req.params.id;
@@ -356,6 +442,7 @@ function getDataToModify( data ){
notes : null,
payment_term : null,
terms_and_conditions : null,
privacy: null
};
let filtered_data = {};
@@ -523,4 +610,4 @@ const deleteLoad = async(req, res) => {
}
};
module.exports = { findCalendarList, findList, getById, patchLoad, postLoad, deleteLoad };
module.exports = { findCalendarList, findList, findDriverList, getById, patchLoad, postLoad, deleteLoad };

View File

@@ -46,7 +46,14 @@ function getAndFilterList( query ){
}
async function getListByType( type , req ){
const filter = { "company_type" : type , "is_hidden" : false };
const filter = {
company_type: type,
is_hidden: false,
$or : [
{ privacy : false },
{ privacy : { $exists : false } }
]
};
const select = [
"rfc",
"company_name",
@@ -58,7 +65,8 @@ async function getListByType( type , req ){
"membership",
"categories",
"truck_type",
"company_description"
"company_description",
"privacy"
];
const { elements } = getPagination( req.query );
const page = 0;// No pagination allowed to this endpoint

View File

@@ -14,7 +14,13 @@ const generic = new GenericHandler( Model, null, populate_list );
const getList = async(req, res) => {
try{
const filter = { status : "Published" };
const filter = {
status: "Published",
$or : [
{ privacy : false },
{ privacy : { $exists : false } }
]
};
const select = [
"shipment_code",
"categories",
@@ -26,7 +32,8 @@ const getList = async(req, res) => {
"est_loading_date",
"est_unloading_date",
"origin",
"destination"
"destination",
"privacy"
];
const { page , elements } = getPagination( req.query );
const retVal = await generic.getList(page , elements, filter, select );

View File

@@ -59,6 +59,7 @@ const schema = new Schema({
lng: { type: String },
is_hidden: { type: Boolean, default: false },
privacy: { type: Boolean, default: false }, /// Disables visibility on the directory, only enabled for private groups.
createdAt: { type : Date, required : true, default : () => { return Date.now(); } }
});

View File

@@ -90,7 +90,9 @@ const schema = new Schema({
payment_term: { type: String },
terms_and_conditions: { type: String },
createdAt: { type : Date, required : true, default : () => { return Date.now(); } }
createdAt: { type : Date, required : true, default : () => { return Date.now(); } },
privacy: { type: Boolean, default: false }, /// Disables visibility on the directory, only enabled for private groups.
});
module.exports = mongoose.model( "loads", schema );