Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,26 @@
*/

import { Response } from 'express';
import { LiteFarmRequest, HttpError } from '../types.js';
import { IrrigationPrescriptionQueryParams } from '../middleware/validation/checkIrrigationPrescription.js';
import { HttpError, ScopeCheckedLiteFarmRequest } from '../types.js';
import { getAddonPartnerIrrigationPrescriptions } from '../services/addonPartner.js';

export interface IrrigationPrescriptionQueryParams {
startTime: string;
endTime: string;
shouldSend: string;
}

const irrigationPrescriptionController = {
getPrescriptions() {
return async (
req: LiteFarmRequest<Required<IrrigationPrescriptionQueryParams>>,
req: ScopeCheckedLiteFarmRequest<IrrigationPrescriptionQueryParams>,
res: Response,
) => {
try {
const { farm_id } = req.headers;
const { startTime, endTime, shouldSend } = req.query;

const irrigationPrescriptions = await getAddonPartnerIrrigationPrescriptions(
// @ts-expect-error - farm_id is guaranteed here by the checkScope middleware with single argument
farm_id,
startTime,
endTime,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,9 @@

import { Response } from 'express';
import { getOrgLocationAndCropData, sendFieldAndCropDataToEsci } from '../util/ensembleService.js';
import { LiteFarmRequest } from '../types.js';
import { HttpError, LiteFarmRequest } from '../types.js';

interface HttpError extends Error {
status?: number;
code?: number; // LF custom error
}

interface InitiateFarmIrrigationPrescriptionQueryParams {
export interface InitiateFarmIrrigationPrescriptionQueryParams {
allOrgs?: string;
shouldSend?: string;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019, 2020, 2021, 2022 LiteFarm.org
* Copyright 2019, 2020, 2021, 2022, 2025 LiteFarm.org
* This file is part of LiteFarm.
*
* LiteFarm is free software: you can redistribute it and/or modify
Expand All @@ -13,13 +13,23 @@
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
*/

import { NextFunction, Response } from 'express';
import { Farm, Permission, RolePermission, User, UserFarm } from '../../models/types.js';
import userFarmModel from '../../models/userFarmModel.js';
import { LiteFarmRequest } from '../../types.js';

const getScopes = async (user_id, farm_id, { checkConsent }) => {
type Scope = UserFarm & RolePermission & Permission;

const getScopes = async (
user_id: User['user_id'],
farm_id: Farm['farm_id'],
{ checkConsent }: { checkConsent: boolean },
): Promise<Scope[]> => {
// essential to fetch the most updated userFarm info to know user's most updated granted access
try {
const permissionQuery = userFarmModel
.query()
.castTo<Scope[]>()
.distinct('permissions.name', 'userFarm.role_id')
.join('rolePermissions', 'userFarm.role_id', 'rolePermissions.role_id')
.join('permissions', 'permissions.permission_id', 'rolePermissions.permission_id')
Expand All @@ -40,25 +50,42 @@ const getScopes = async (user_id, farm_id, { checkConsent }) => {
* @param expectedScopes - array of required scopes to make request [ 'get:crops', 'add:sales' ]
* @param checkConsent {boolean}
*/
const checkScope = (expectedScopes, { checkConsent = true } = {}) => {
const checkScope = (
expectedScopes?: string[],
{ checkConsent = true }: { checkConsent?: boolean } = {},
) => {
if (!Array.isArray(expectedScopes)) {
throw new Error(
'Parameter expectedScopes must be an array of strings representing the scopes for the endpoint(s)',
);
}

return async (req, res, next) => {
return async (req: LiteFarmRequest, res: Response, next: NextFunction) => {
if (expectedScopes.length === 0) {
return next();
}
const { headers } = req;

// Check auth
// NOTE: Consider making this a separate middleware with checkJwt
if (!req.auth) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did not exist here prior to this PR.

return res.status(400).send('No Auth provided');
}
const { user_id } = req.auth;
const { farm_id } = headers; // these are the minimum props needed for most endpoints' authorization
if (!user_id || user_id === 'undefined') {
return res.status(400).send('Missing user_id in auth');
}

if (!user_id || user_id === 'undefined')
return res.status(400).send('Missing user_id in headers');
if (!farm_id || farm_id === 'undefined')
// Check headers
if (!req.headers) {
return res.status(400).send('Missing headers');
}

const { farm_id } = req.headers; // these are the minimum props needed for most endpoints' authorization

if (!farm_id || farm_id === 'undefined') {
return res.status(400).send('Missing farm_id in headers');
}

try {
const scopes = await getScopes(user_id, farm_id, { checkConsent });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,14 @@
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
*/

import { Request, Response, NextFunction } from 'express';
import { Response, NextFunction } from 'express';
import { isISO8601Format } from '../../util/validation.js';

export interface IrrigationPrescriptionQueryParams {
startTime?: string;
endTime?: string;
shouldSend?: string;
}
import { IrrigationPrescriptionQueryParams } from '../../controllers/irrigationPrescriptionController.js';
import { ScopeCheckedLiteFarmRequest } from '../../types.js';

export function checkGetIrrigationPrescription() {
return async (
req: Request<unknown, unknown, unknown, IrrigationPrescriptionQueryParams>,
req: ScopeCheckedLiteFarmRequest<Partial<IrrigationPrescriptionQueryParams>>,
res: Response,
next: NextFunction,
) => {
Expand Down
69 changes: 66 additions & 3 deletions packages/api/src/models/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ export enum GENDER {
FEMALE = 'FEMALE',
}

// Table with no model
type UserStatus = {
status_id: number;
status_description: string;
};
export interface User extends Timestamps {
user_id: string;
first_name: string;
Expand All @@ -60,7 +65,7 @@ export interface User extends Timestamps {
sandbox_user: boolean;
notification_setting: UserNotificationSetting;
language_preference: string;
status_id: number; // TODO: user status model does not exist
status_id: UserStatus['status_id'];
gender: GENDER;
birth_year: number;
do_not_email: boolean;
Expand All @@ -71,9 +76,11 @@ interface UserTimeStamps extends Timestamps {
updated_by_user_id: User['user_id'];
}

interface BaseProperties extends UserTimeStamps {
type SoftDelete = {
deleted: boolean;
}
};

interface BaseProperties extends UserTimeStamps, SoftDelete {}

enum FarmUnitSystem {
IMPERIAL = 'imperial',
Expand Down Expand Up @@ -594,3 +601,59 @@ export interface FarmAddon extends BaseProperties {
org_uuid: string;
org_pk: number;
}

export interface Role extends SoftDelete {
role_id: number;
role: string;
}

// Table with no model
export type Permission = {
permission_id: number;
name: string;
description: string;
};

// Table with no model
export type RolePermission = {
role_id: Role['role_id'];
permission_id: Permission['permission_id'];
};

enum UserFarmStatus {
ACTIVE = 'Active',
INACTIVE = 'Inactive',
INVITED = 'Invited',
}

enum WageRateUnit {
HOURLY = 'hourly',
ANNUALLY = 'annually',
}

type Wage = {
type: WageRateUnit;
amount?: number;
};

// NOTE: Why does userFarm not have updated_at?
export interface UserFarm extends Pick<Timestamps, 'created_at'> {
user_id: User['user_id'];
farm_id: Farm['farm_id'];
role_id: Role['role_id'];
has_consent: boolean;
status: UserFarmStatus;
consent_version: string;
wage?: Wage;
step_one?: boolean;
step_one_end?: string;
step_two?: boolean;
step_two_end?: string;
step_three?: boolean;
step_three_end?: string;
step_four?: boolean;
step_four_end?: string;
step_five?: boolean;
step_five_end?: string;
wage_do_not_ask_again?: boolean;
}
15 changes: 9 additions & 6 deletions packages/api/src/routes/irrigationPrescriptionRequestRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,20 @@

import express from 'express';
import checkScope from '../middleware/acl/checkScope.js';
import IrrigationPrescriptionRequestController from '../controllers/irrigationPrescriptionRequestController.js';
import IrrigationPrescriptionRequestController, {
InitiateFarmIrrigationPrescriptionQueryParams,
} from '../controllers/irrigationPrescriptionRequestController.js';
import { ScopeCheckedLiteFarmRequest } from '../types.js';
import checkSchedulerJwt from '../middleware/acl/checkSchedulerJwt.js';
import checkSchedulerPermission from '../middleware/acl/checkSchedulerPermission.js';

const router = express.Router();

router.post(
'/',
checkScope(['get:smart_irrigation']),
IrrigationPrescriptionRequestController.initiateFarmIrrigationPrescription(),
);
router.post('/', checkScope(['get:smart_irrigation']), (req, res) => {
const typedReq =
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main change 1...

req as ScopeCheckedLiteFarmRequest<InitiateFarmIrrigationPrescriptionQueryParams>;
IrrigationPrescriptionRequestController.initiateFarmIrrigationPrescription()(typedReq, res);
});

router.post(
'/scheduler',
Expand Down
15 changes: 12 additions & 3 deletions packages/api/src/routes/irrigationPrescriptionRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,25 @@

import express from 'express';
import checkScope from '../middleware/acl/checkScope.js';
import IrrigationPrescriptionController from '../controllers/irrigationPrescriptionController.js';
import IrrigationPrescriptionController, {
IrrigationPrescriptionQueryParams,
} from '../controllers/irrigationPrescriptionController.js';
import { ScopeCheckedLiteFarmRequest } from '../types.js';
import { checkGetIrrigationPrescription } from '../middleware/validation/checkIrrigationPrescription.js';

const router = express.Router();

router.get(
'/',
checkScope(['get:smart_irrigation']),
checkGetIrrigationPrescription(),
IrrigationPrescriptionController.getPrescriptions(),
(req, res, next) => {
const typedReq = req as ScopeCheckedLiteFarmRequest<Partial<IrrigationPrescriptionQueryParams>>;
checkGetIrrigationPrescription()(typedReq, res, next);
},
(req, res) => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main change 2...

const typedReq = req as ScopeCheckedLiteFarmRequest<IrrigationPrescriptionQueryParams>;
IrrigationPrescriptionController.getPrescriptions()(typedReq, res);
},
);

export default router;
54 changes: 45 additions & 9 deletions packages/api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,60 @@
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
*/

import { NextFunction, Request, Response } from 'express';
import { Request } from 'express';
import { Farm, Point, Role, Task, TaskType, User } from './models/types.js';

export interface HttpError extends Error {
status?: number;
code?: number; // LF custom error
}

// TODO: Remove farm_id conditional and cast this in a checkScope() that takes the function and casts this to req
/**
* For use with unchecked requests
*
* All possible shapes of a litefarm 'req' object
*/
export interface LiteFarmRequest<QueryParams = unknown>
extends Request<unknown, unknown, unknown, QueryParams> {
auth?: {
user_id?: User['user_id'];
farm_id?: Farm['farm_id'];
sub?: User['user_id'];
email?: User['email'];
given_name?: User['first_name'];
family_name?: User['last_name'];
first_name?: User['first_name'];
language_preference?: string;
};
headers: Request['headers'] & {
farm_id?: string;
user_id?: User['user_id'];
farm_id?: Farm['farm_id'];
};
role?: Role['role_id'];
isMinimized?: boolean;
isTextDocument?: boolean;
isNotMinimized?: boolean;
field?: { fieldId?: string | number; point?: Point };
file?: unknown;
checkTaskStatus?: {
complete_date?: Task['complete_date'];
abandon_date?: Task['abandon_date'];
assignee_user_id?: Task['assignee_user_id'];
task_translation_key?: TaskType['task_translation_key'];
};
}

// Can be used to cast after checkScope() succeeds
export type LiteFarmHandler<QueryParams = unknown> = (
req: LiteFarmRequest<QueryParams>,
res: Response,
next: NextFunction,
) => void | Promise<void>;
/**
* For use after checkScope() middleware.
*
* DO NOT add more required props unless it is auth related, make a new type
*/

export interface ScopeCheckedLiteFarmRequest<ReqQuery = unknown> extends LiteFarmRequest<ReqQuery> {
auth: {
user_id: User['user_id'];
};
headers: Request['headers'] & {
farm_id: string;
};
}