diff --git a/backend/package.json b/backend/package.json index 2f4abdbda..98dd1160b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,12 +1,22 @@ { - "name": "OpenHarvest-backend", + "name": "@openharvest/backend", "version": "1.0.0", "description": "", "main": "main.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "nodemon", - "build": "tsc", + "build-ts": "tsc --build", + "build": "npm run build-ts && npm run lint", + "debug": "npm run build && npm run watch-debug", + "lint": "tsc --noEmit && eslint \"**/*.{js,ts}\" --quiet --fix", + "serve-debug": "nodemon --inspect dist/server.js", + "serve": "node dist/server.js", + "start": "npm run serve", + "watch-debug": "concurrently -k -p \"[{name}]\" -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold,green.bold\" \"npm run watch-ts\" \"npm run serve-debug\"", + "watch-node": "nodemon dist/server.js", + "watch-test": "npm run test -- --watchAll", + "watch-ts": "tsc --build -w ", + "watch": "npm run build-ts && concurrently -k -p \"[{name}]\" -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold,green.bold\" \"npm run watch-ts\" \"npm run watch-node\"", "mkcert": "mkcert localhost 127.0.0.1" }, "keywords": [], @@ -30,11 +40,238 @@ "uuid": "^8.3.2" }, "devDependencies": { + "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.12", "@types/express": "^4.17.13", "@types/express-session": "^1.17.4", + "@types/passport": "^1.0.7", + "concurrently": "^7.2.0", "nodemon": "^2.0.15", "ts-node": "^10.5.0", "typescript": "^4.5.5" + }, + "eslintConfig": { + "overrides": [ + { + "files": [ + "**/*.ts?(x)" + ], + "extends": [ + "plugin:react/recommended" + ], + "plugins": [ + "eslint-plugin-prefer-arrow", + "eslint-plugin-react", + "@typescript-eslint" + ], + "rules": { + "@typescript-eslint/adjacent-overload-signatures": "warn", + "@typescript-eslint/array-type": [ + "warn", + { + "default": "array" + } + ], + "@typescript-eslint/ban-types": [ + "warn", + { + "types": { + "Object": { + "message": "Avoid using the `Object` type. Did you mean `object`?" + }, + "Function": { + "message": "Avoid using the `Function` type. Prefer a specific function type, like `() => void`." + }, + "Boolean": { + "message": "Avoid using the `Boolean` type. Did you mean `boolean`?" + }, + "Number": { + "message": "Avoid using the `Number` type. Did you mean `number`?" + }, + "String": { + "message": "Avoid using the `String` type. Did you mean `string`?" + }, + "Symbol": { + "message": "Avoid using the `Symbol` type. Did you mean `symbol`?" + } + } + } + ], + "@typescript-eslint/consistent-type-assertions": "warn", + "@typescript-eslint/dot-notation": "off", + "@typescript-eslint/explicit-function-return-type": "warn", + "@typescript-eslint/explicit-member-accessibility": [ + "off", + { + "accessibility": "explicit" + } + ], + "@typescript-eslint/indent": "warn", + "@typescript-eslint/member-delimiter-style": [ + "warn", + { + "multiline": { + "delimiter": "semi", + "requireLast": true + }, + "singleline": { + "delimiter": "semi", + "requireLast": false + } + } + ], + "@typescript-eslint/member-ordering": "warn", + "@typescript-eslint/naming-convention": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-empty-interface": "warn", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-inferrable-types": [ + "warn", + { + "ignoreParameters": true + } + ], + "@typescript-eslint/no-misused-new": "warn", + "@typescript-eslint/no-namespace": "warn", + "@typescript-eslint/no-parameter-properties": "off", + "@typescript-eslint/no-shadow": [ + "warn", + { + "hoist": "all" + } + ], + "@typescript-eslint/no-unused-expressions": "warn", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/no-var-requires": "warn", + "@typescript-eslint/prefer-for-of": "warn", + "@typescript-eslint/prefer-function-type": "warn", + "@typescript-eslint/prefer-namespace-keyword": "warn", + "@typescript-eslint/quotes": [ + "warn", + "double" + ], + "@typescript-eslint/semi": [ + "warn", + "always" + ], + "@typescript-eslint/triple-slash-reference": [ + "warn", + { + "path": "always", + "types": "prefer-import", + "lib": "always" + } + ], + "@typescript-eslint/type-annotation-spacing": "warn", + "@typescript-eslint/unified-signatures": "warn", + "brace-style": [ + "warn", + "1tbs" + ], + "complexity": "off", + "constructor-super": "warn", + "curly": "warn", + "eol-last": "warn", + "eqeqeq": [ + "warn", + "smart" + ], + "guard-for-in": "warn", + "id-blacklist": [ + "warn", + "any", + "Number", + "number", + "String", + "string", + "Boolean", + "boolean", + "Undefined", + "undefined" + ], + "id-match": "warn", + "indent": "off", + "max-classes-per-file": [ + "warn", + 1 + ], + "max-len": [ + "warn", + { + "code": 200 + } + ], + "new-parens": "warn", + "no-bitwise": "warn", + "no-caller": "warn", + "no-cond-assign": "warn", + "no-console": [ + "warn", + { + "allow": [ + "log", + "warn", + "dir", + "timeLog", + "assert", + "clear", + "count", + "countReset", + "group", + "groupEnd", + "table", + "dirxml", + "error", + "groupCollapsed", + "Console", + "profile", + "profileEnd", + "timeStamp", + "context" + ] + } + ], + "no-debugger": "warn", + "no-empty": "off", + "no-eval": "warn", + "no-fallthrough": "warn", + "no-invalid-this": "off", + "no-new-wrappers": "warn", + "no-redeclare": "warn", + "no-restricted-imports": "warn", + "no-throw-literal": "warn", + "no-trailing-spaces": "warn", + "no-undef-init": "warn", + "no-underscore-dangle": "off", + "no-unsafe-finally": "warn", + "no-unused-labels": "warn", + "no-var": "warn", + "object-shorthand": "warn", + "one-var": [ + "warn", + "never" + ], + "prefer-arrow/prefer-arrow-functions": "warn", + "prefer-const": "warn", + "radix": "warn", + "react/jsx-boolean-value": "warn", + "react/jsx-key": "warn", + "react/jsx-no-bind": "warn", + "react/self-closing-comp": "warn", + "spaced-comment": [ + "warn", + "always", + { + "markers": [ + "/" + ] + } + ], + "use-isnan": "warn", + "valid-typeof": "off", + "react/display-name": "warn" + } + } + ] } } diff --git a/backend/src/auth/IBMiDStrategy.ts b/backend/src/auth/IBMiDStrategy.ts index e5d192127..a0e586331 100644 --- a/backend/src/auth/IBMiDStrategy.ts +++ b/backend/src/auth/IBMiDStrategy.ts @@ -1,6 +1,5 @@ import { IDaaSOIDCStrategy as OpenIDConnectStrategy } from "passport-ci-oidc"; -import { getCoopManager } from "./../services/coopManager.service"; -import { getOrganisations } from "./../services/organisation.service"; +import { userService } from "../services/UserService"; export const IBMidStrategy = new OpenIDConnectStrategy({ discoveryURL: process.env.AUTH_discovery_url, @@ -11,25 +10,24 @@ export const IBMidStrategy = new OpenIDConnectStrategy({ callbackURL: process.env.AUTH_callback_url, skipUserProfile: true}, // Add your own data here. + // @ts-ignore function (iss, sub, profile, accessToken, refreshToken, params, done) { process.nextTick(async function () { profile.accessToken = accessToken; profile.refreshToken = refreshToken; // Get the farmer coop details const id = "IBMid:" + profile.id; - const doc = await getCoopManager(id); + const doc = await userService.getUser(id); if (doc) { profile.isOnboarded = true; - profile.coopManager = doc.toObject(); - profile.organisations = await getOrganisations(doc.coopOrganisations); - profile.selectedOrganisation = profile.organisations[0]; + profile.user = doc; } else { profile.isOnboarded = false; - profile.coopManager = null; + profile.user = null; } console.log(profile); done(null, profile); }); } -) \ No newline at end of file +) diff --git a/backend/src/auth/helpers.ts b/backend/src/auth/helpers.ts index 728a0aa7a..640f60895 100644 --- a/backend/src/auth/helpers.ts +++ b/backend/src/auth/helpers.ts @@ -1,5 +1,4 @@ -import { CoopManager } from "./../db/entities/coopManager"; -import { Organisation } from "./../db/entities/organisation"; +import { Organisation, User } from "../../../common-types/src"; export interface CoopManagerUser { id: string; @@ -15,7 +14,7 @@ export interface CoopManagerUser { exp: number; accessToken: string; refreshToken: string; - coopManager: CoopManager | null; + coopManager: User | null; organisations: Organisation[]; selectedOrganisation: Organisation; } @@ -41,6 +40,7 @@ export function formatUser(user: any): CoopManagerUser { } } +// @ts-ignore export function ensureAuthenticated(req, res, next) { // console.log(req); if (!req.isAuthenticated()) { diff --git a/backend/src/db/entities/coopManager.ts b/backend/src/db/entities/coopManager.ts deleted file mode 100644 index baf91f6fd..000000000 --- a/backend/src/db/entities/coopManager.ts +++ /dev/null @@ -1,33 +0,0 @@ - -import { Schema, model, ObjectId, Types } from 'mongoose'; -import { Land } from './land'; - -const ObjectId = Schema.Types.ObjectId; - -export interface CoopManager { - /** - * Auth provider + auth provider id. E.g. "IBMid:1SDAS61W6A" - */ - _id?: string, - /** - * GeoCode / LatLng coordinate tuple - */ - location: number[], - mobile: string, - coopOrganisations: string[] -} - -export const CoopManagerSchema = new Schema({ - /** - * Auth provider + auth provider id. E.g. "IBMid:1SDAS61W6A" - */ - _id: String, - /** - * GeoCode / LatLng coordinate tuple - */ - location: [Number], - mobile: String, - coopOrganisations: [String] -}); - -export const CoopManagerModel = model("coopManager", CoopManagerSchema); diff --git a/backend/src/db/entities/crop.ts b/backend/src/db/entities/crop.ts index 44cc8b66b..51d0050fa 100644 --- a/backend/src/db/entities/crop.ts +++ b/backend/src/db/entities/crop.ts @@ -1,28 +1,16 @@ +import { model, Schema, Types } from 'mongoose'; -import { Schema, model, ObjectId, Types } from 'mongoose'; - -const ObjectId = Schema.Types.ObjectId; - -export interface Crop { - _id?: Types.ObjectId, - type: string, - name: string, - // Start Day of year to end Day of year when to plant the ground nuts - planting_season: number[], - time_to_harvest: number, - is_ongoing: boolean, - yield_per_sqm: number -} +import { Crop } from "../../../../common-types/src" // Mongoose will automatically add _id property. -export const CropSchema = new Schema({ +export const CropSchema = new Schema({ + _id: Types.ObjectId, type: String, name: String, // Start Day of year to end Day of year when to plant the ground nuts planting_season: [Number], time_to_harvest: Number, - is_ongoing: Boolean, yield_per_sqm: Number }); -export const CropModel = model("crop", CropSchema); \ No newline at end of file +export const CropModel = model("crop", CropSchema); diff --git a/backend/src/db/entities/farm.ts b/backend/src/db/entities/farm.ts new file mode 100644 index 000000000..4c71a697f --- /dev/null +++ b/backend/src/db/entities/farm.ts @@ -0,0 +1,27 @@ +import { model, Schema, Types } from 'mongoose'; + +import { CropSchema } from "./crop"; +import { PolygonSchema } from "../mongodb"; +import { Field, FieldCrop, NewFarm } from '../../../../common-types/src'; + +export const FieldCropSchema = new Schema({ + crop: CropSchema, + planted_date: Date, + harvested_date: Date +}, {id: false}); + +export const FieldSchema = new Schema({ + _id: Types.ObjectId, + name: String, + crops: [FieldCropSchema], + geometry: PolygonSchema, +}); + +export const FarmSchema = new Schema({ + _id: Types.ObjectId, + name: String, + fields: [FieldSchema], + geometry: PolygonSchema, +}); + +export const FarmModel = model("farm", FarmSchema); diff --git a/backend/src/db/entities/farmer.ts b/backend/src/db/entities/farmer.ts index e34e97810..9994c75c1 100644 --- a/backend/src/db/entities/farmer.ts +++ b/backend/src/db/entities/farmer.ts @@ -1,30 +1,14 @@ +import { model, Schema, Types } from 'mongoose'; +import { Farmer } from '../../../../common-types/src'; +import { FarmSchema } from "./farm"; -import { FieldResponse } from './../../integrations/EIS/EIS.types'; -import { Schema, model, ObjectId, Types } from 'mongoose'; -import { Land } from './land'; - -const ObjectId = Schema.Types.ObjectId; - -export interface Farmer { - _id?: Types.ObjectId, - name: string, - mobile: string, - address: string, - coopOrganisations: string[], - fieldCount: number; - field?: FieldResponse; -} - -export const FarmerSchema = new Schema({ - _id: { - type: ObjectId, - auto: true - }, +export const FarmerSchema = new Schema({ + _id: Types.ObjectId, name: String, mobile: String, address: String, - coopOrganisations: [String], - fieldCount: Number + organisation: String, + farms: [FarmSchema] }); -export const FarmerModel = model("farmer", FarmerSchema); \ No newline at end of file +export const FarmerModel = model("farmer", FarmerSchema); diff --git a/backend/src/db/entities/land.ts b/backend/src/db/entities/land.ts deleted file mode 100644 index b2a1e2474..000000000 --- a/backend/src/db/entities/land.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Using Node.js `require()` -import { Schema, Model, model, Types } from 'mongoose'; -import { CropSchema, Crop } from "./crop"; -import { FarmerSchema, Farmer } from './farmer'; - -const ObjectId = Schema.Types.ObjectId; - -export interface FarmerCrop { - _id?: Types.ObjectId, - farmer: Farmer, - crop: Crop -} - -export const FarmerCropSchema = new Schema({ - _id: ObjectId, - farmer: FarmerSchema, - crop: CropSchema -}); - -export interface Land { - _id?: Types.ObjectId, - type: string, - fid: number, - name: string, - crops: FarmerCrop[] -} - -export const LandSchema = new Schema({ - _id: ObjectId, - type: String, - fid: Number, - name: String, - crops: [FarmerCropSchema] -}); - -export const LandModel = model("land", LandSchema); \ No newline at end of file diff --git a/backend/src/db/entities/messageLog.ts b/backend/src/db/entities/messageLog.ts index a5a633fee..6e6e0ae38 100644 --- a/backend/src/db/entities/messageLog.ts +++ b/backend/src/db/entities/messageLog.ts @@ -1,5 +1,4 @@ - -import { Schema, model, ObjectId, Types } from 'mongoose'; +import { model, Schema, Types } from 'mongoose'; const ObjectId = Schema.Types.ObjectId; @@ -56,4 +55,4 @@ export const MessageLogSchema = new Schema({ messageRef: String }); -export const MessageLogModel = model("messageLog", MessageLogSchema); \ No newline at end of file +export const MessageLogModel = model("messageLog", MessageLogSchema); diff --git a/backend/src/db/entities/organisation.ts b/backend/src/db/entities/organisation.ts index 0ec005eb2..e7818fdb5 100644 --- a/backend/src/db/entities/organisation.ts +++ b/backend/src/db/entities/organisation.ts @@ -1,20 +1,26 @@ +import { model, Schema } from 'mongoose'; +import { Organisation, User } from '../../../../common-types/src'; -import { Schema, model, ObjectId, Types } from 'mongoose'; -import { Land } from './land'; +import { PointSchema } from "../mongodb"; -const ObjectId = Schema.Types.ObjectId; +export const UserSchema = new Schema({ + /** + * Auth provider + auth provider id. E.g. "IBMid:1SDAS61W6A" + */ + _id: String, + location: PointSchema, + mobile: String, +}); -export interface Organisation { - _id?: Types.ObjectId, - name: string -} -export const OrganisationSchema = new Schema({ - _id: { - type: ObjectId, - auto: true +export const OrganisationSchema = new Schema({ + name: { + type: String, + unique: true, + required: true, + index: true }, - name: String, -}); + users: [UserSchema] +}, { _id : false }); export const OrganisationModel = model("organisation", OrganisationSchema); diff --git a/backend/src/db/mongodb.ts b/backend/src/db/mongodb.ts index b4e313d70..1eac9649a 100644 --- a/backend/src/db/mongodb.ts +++ b/backend/src/db/mongodb.ts @@ -1,6 +1,5 @@ // Using Node.js `require()` -import { connect } from 'mongoose'; -import { writeFile } from "fs/promises"; +import { connect, Schema } from 'mongoose'; export async function mongoInit() { // console.log(process.env.mongodb_url); @@ -20,8 +19,31 @@ export async function mongoInit() { else { await connect(process.env.mongodb_url!!); } - console.log("Connected to DB"); } +export const PointSchema = new Schema({ + type: { + type: String, + enum: ['Point'], + required: true + }, + coordinates: { + type: [Number], + required: true + } +}); + +export const PolygonSchema = new Schema({ + type: { + type: String, + enum: ['Polygon'], + required: true + }, + coordinates: { + type: [[[Number]]], // Array of arrays of arrays of numbers + required: true + } +}); + diff --git a/backend/src/declarations.d.ts b/backend/src/declarations.d.ts new file mode 100644 index 000000000..6e492060a --- /dev/null +++ b/backend/src/declarations.d.ts @@ -0,0 +1 @@ +declare module "passport-ci-oidc"; diff --git a/backend/src/integrations/EIS/EIS-api.service.ts b/backend/src/integrations/EIS/EIS-api.service.ts deleted file mode 100644 index fd6d87e84..000000000 --- a/backend/src/integrations/EIS/EIS-api.service.ts +++ /dev/null @@ -1,107 +0,0 @@ -import axios from "axios"; -import { EISField, EISFieldCreateResponse, EISSubFieldSearchReturn, FieldResponse, Fields } from "./EIS.types"; - - -export class EISAPIService { - - access_token = ""; - /** - * This is a millisecond based timestamp of when the token is due to expire. - * If we're 10 minutes away from it we renew the token - */ - expiration = 0; - - baseAPI = "https://foundation.agtech.ibm.com/v2/"; - - authAxios = axios.create({}); - - constructor(private apiKey: string) { - - } - - - async ensureToken(): Promise { - // 10 mins earlier we try to renew the token - if (this.expiration == 0 || Date.now() > (this.expiration - 600000)) { - // Token is expired - const res = await axios.post("https://auth-b2b-twc.ibm.com/Auth/GetBearerForClient", { - "apiKey": this.apiKey, - "clientId": "ibm-agro-api" - }); - - this.access_token = res.data.access_token; - this.expiration = Date.now() + (res.data.expires_in * 1000); - - this.authAxios.defaults.headers.common['Authorization'] = `Bearer ${this.access_token}`; - - return true; - } - return false; - } - - async createField(field: EISField): Promise { - await this.ensureToken(); - - const res = await this.authAxios.post(this.baseAPI + "field", field); - return res.data; - } - - async getFields() { - await this.ensureToken(); - const res = await this.authAxios.get(this.baseAPI + "field"); - return res.data - } - - async getField(uuid: string) { - await this.ensureToken(); - const fieldRes = await this.authAxios.get(this.baseAPI + `field/${uuid}`); - - const field = fieldRes.data; - - // We need to convert the OpenHarvest object from a string to JSON because EIS stores it as a string - for (let i = 0; i < field.subFields.features.length; i++) { - const subField = field.subFields.features[i]; - subField.properties.open_harvest = JSON.parse(subField.properties.open_harvest as any); - } - - return fieldRes.data; - } - - async getFarmerField(farmer_id: string): Promise { - await this.ensureToken(); - - const queryBody = { - "uuidsOnly": false, - "inputType": "SPECIFIED_FIELD", - "includeDeleted": true, - "includeAssetGeometry": true, - "properties": { - open_harvest: { - farmer_id - } - } - }; - - const res = await this.authAxios.post(this.baseAPI + "asset/search", queryBody); - const subfields = res.data; - - if (subfields.totalRecords == 0) { - return null; - } - - // Get the parent reference which points to the field uuid - const fieldUuid = subfields.features[0].parentReference; - - try { - return await this.getField(fieldUuid); - } - catch (e) { - console.error(e); - return null; - } - } - - - - -} \ No newline at end of file diff --git a/backend/src/integrations/EIS/EIS.types.ts b/backend/src/integrations/EIS/EIS.types.ts index 6f988246c..ae3060fb7 100644 --- a/backend/src/integrations/EIS/EIS.types.ts +++ b/backend/src/integrations/EIS/EIS.types.ts @@ -1,6 +1,5 @@ -import { Crop } from "../../db/entities/crop"; -import { Feature, FeatureCollection, Geometry, Polygon } from "geojson"; -import { LatLng } from "integrations/weather-company-api.types"; +import { Feature, FeatureCollection, Polygon } from "geojson"; +import { FieldCrop, GeoCodeNumber } from "../../../../common-types/src"; /** * There are many redundant fields you'll notice because EIS's data structures @@ -37,7 +36,7 @@ export interface FieldResponseSubfieldFeatureExtras { projection: 4326; } -export type FieldResponseSubfieldFeature = Feature & FieldResponseSubfieldFeatureExtras; +export type FieldResponseSubfieldFeature = Feature & FieldResponseSubfieldFeatureExtras; export interface FieldResponseSubfield { type: "FeatureCollection", @@ -51,16 +50,10 @@ export interface FieldResponse { subFields: FieldResponseSubfield; } -// Field Create Structures -export interface SubFieldCrop { - planted: Date; - harvested: Date | null; - farmer: string; - /** - * Crop Information - */ - crop: Crop; -} +export type OpenHarvestSubFieldProps = { + farmer_id: string; + crops: FieldCrop[] +}; export interface EISSubFieldProperties { farm_name: string @@ -68,10 +61,7 @@ export interface EISSubFieldProperties { /** * When getting a field from EIS this is initially a string and we need to parse the object */ - open_harvest: { - farmer_id: string; - crops: SubFieldCrop[] - } + open_harvest: OpenHarvestSubFieldProps } export interface EISSubField { @@ -109,7 +99,7 @@ export interface EISSubFieldSearchReturnFeatureProperties { east: number; west: number; }; - centroid: LatLng; + centroid: GeoCodeNumber; ianaTimeZone: string; deleted: boolean; inputType: string; @@ -118,10 +108,7 @@ export interface EISSubFieldSearchReturnFeatureProperties { farm_name: string; field_id: string; field_name: string; - open_harvest: { - farmer_id: string; - crops: SubFieldCrop[] - } + open_harvest: OpenHarvestSubFieldProps } export interface EISSubFieldSearchReturnFeatureExtras { diff --git a/backend/src/integrations/EIS/EISFarmService.ts b/backend/src/integrations/EIS/EISFarmService.ts new file mode 100644 index 000000000..d17f1a5d4 --- /dev/null +++ b/backend/src/integrations/EIS/EISFarmService.ts @@ -0,0 +1,116 @@ +import { EISConfig, Farm, Farmer, Field, NewFarm, NewField } from "../../../../common-types/src"; +import { EISField, EISFieldCreateResponse, EISSubField, EISSubFieldProperties, EISSubFieldSearchReturn, FieldResponse, OpenHarvestSubFieldProps } from "./EIS.types"; +import { EISService } from "./EISService"; +import { Feature, FeatureCollection, Polygon } from "geojson"; + +export class EISFarmService extends EISService { + + async getFarmerFarms(eisConfig: EISConfig, farmer: Farmer): Promise { + const eisSession = await this.getToken(eisConfig); + + const queryBody = { + "uuidsOnly": false, + "inputType": "SPECIFIED_FIELD", + "includeDeleted": true, + "includeAssetGeometry": true, + "properties": { + open_harvest: { + farmer_id: farmer?._id + } + } + }; + + const res = await eisSession.authAxios.post(eisSession.eisConfig.apiUrl + "asset/search", queryBody); + const subfields = res.data; + + if (subfields.totalRecords == 0) { + return []; + } + + // Get the parent reference which points to the field uuid + const parentRefs: {[name: string] : string} = {}; + + subfields.features.forEach((subField) => { + parentRefs[subField.parentReference] = subField.parentReference; + }); + + const farms: Farm[] = []; + + try { + for (const parentRef of Object.keys(parentRefs)) { + farms.push(await this.getFarm(eisConfig, parentRef)); + } + } catch (e) { + console.error(e); + } + return farms; + } + + async saveFarm(eisConfig: EISConfig, farmer: Farmer, farm: NewFarm): Promise { + const eisSession = await this.getToken(eisConfig); + + const eisField: EISField = { + name: farm.name, + subFields: farm.fields.map((field) => EISFarmService.convertToEisSubField(farmer, field)) + }; + + const res = await eisSession.authAxios.post(eisSession.eisConfig.apiUrl + "field", eisField); + + return this.getFarm(eisConfig, res.data.field); + } + + private async getFarm(eisConfig: EISConfig, uuid: string): Promise { + const eisSession = await this.getToken(eisConfig); + + const fieldRes = await eisSession.authAxios.get(eisSession.eisConfig.apiUrl + `field/${uuid}`); + + const fieldResponse: FieldResponse = fieldRes.data; + + const fields: Field[] = []; + for (const subField of fieldResponse.subFields.features) { + // We need to convert the open harvest object from a string to JSON because EIS stores it as a string + const openHarvestProps: OpenHarvestSubFieldProps = JSON.parse(subField.properties.open_harvest as any); + + let field: Field = { + _id: subField.uuid, + geometry: subField.geometry, + name: subField.properties.field_name, + crops: openHarvestProps.crops + }; + + fields.push(field); + } + + return {_id: uuid, fields, name: fieldResponse.properties.name}; + } + + private static convertToEisSubField(farmer: Farmer, field: NewField): EISSubField { + const feature: Feature = { + geometry: field.geometry, + properties: { + farm_name: field.name, + open_harvest_farmer_id: farmer._id, + open_harvest: { + farmer_id: farmer._id, + crops: field.crops + } + }, + type: "Feature" + } + + const featureCollection: FeatureCollection = { + features: [feature], + type: "FeatureCollection" + } + + return { + geo: { + geojson: featureCollection, + type: "geojson" + }, + name: field.name + } + } +} + +export const eisFarmService: EISFarmService = new EISFarmService(); diff --git a/backend/src/integrations/EIS/EISService.ts b/backend/src/integrations/EIS/EISService.ts new file mode 100644 index 000000000..1915f751b --- /dev/null +++ b/backend/src/integrations/EIS/EISService.ts @@ -0,0 +1,53 @@ +import axios, { AxiosInstance } from "axios"; +import { EISConfig, isUndefined } from "../../../../common-types/src"; + +type EISSession = { + token: string, + expiration: number, + authAxios: AxiosInstance, + eisConfig: EISConfig +}; + +export abstract class EISService { + private readonly accessTokens: { + [name: string]: EISSession + } = {}; + + /** + * This is a millisecond based timestamp of when the token is due to expire. + * If we're 10 minutes away from it, we renew the token + */ + + // baseAPI = "https://foundation.agtech.ibm.com/v2/"; + //tokenUrl = "https://auth-b2b-twc.ibm.com/Auth/GetBearerForClient"; + + async getToken(eisConfig: EISConfig): Promise { + + const accessTokenKey = `${eisConfig.clientId}:${eisConfig.apiKey}`; + + const accessToken: EISSession = this.accessTokens[accessTokenKey]; + + // 10 minutes earlier we try to renew the token + if (isUndefined(accessToken) || accessToken.expiration == 0 || Date.now() > (accessToken.expiration - 600000)) { + // Token is expired + const res = await axios.post(eisConfig.tokenUrl, { + "apiKey": eisConfig.apiKey, + "clientId": eisConfig.clientId // "ibm-agro-api" + }); + + const authAxios = accessToken?.authAxios || axios.create({}); + const newToken = res.data.access_token; + authAxios.defaults.headers.common['Authorization'] = `Bearer ${newToken}` + + this.accessTokens[accessTokenKey] = { + token: newToken, + expiration: Date.now() + (res.data.expires_in * 1000), + authAxios, + eisConfig + }; + } + return this.accessTokens[accessTokenKey]; + } + +} + diff --git a/backend/src/integrations/eventBus.service.ts b/backend/src/integrations/eventBus.service.ts index 95856e3e0..8ef5b8ebe 100644 --- a/backend/src/integrations/eventBus.service.ts +++ b/backend/src/integrations/eventBus.service.ts @@ -5,11 +5,11 @@ * will make the move easy as it will just become an interface */ +import { Organisation } from "../../../common-types/src"; import { EventEmitter } from "events"; -import { Organisation } from "./../db/entities/organisation"; -import { MessageLog } from "./../db/entities/messageLog"; -import { SocketIOManagerInstance } from "./../sockets/socket.io"; +import { MessageLog } from "../db/entities/messageLog"; +import { SocketIOManagerInstance } from "../sockets/socket.io"; export declare interface EventBus { on(event: 'onMessage', listener: (message: MessageLog) => void): this; diff --git a/backend/src/integrations/messagingInterface.ts b/backend/src/integrations/messagingInterface.ts index ddb44e807..a6278ecb3 100644 --- a/backend/src/integrations/messagingInterface.ts +++ b/backend/src/integrations/messagingInterface.ts @@ -1,9 +1,9 @@ import { EventEmitter } from "events"; -import { Farmer, FarmerModel } from "./../db/entities/farmer"; -import { MessageLog } from "./../db/entities/messageLog"; -import { CoopManager } from "./../db/entities/coopManager"; -import { EventBusInstance } from "./../integrations/eventBus.service"; -import { OrganisationModel } from "./../db/entities/organisation"; +import { FarmerModel } from "../db/entities/farmer"; +import { MessageLog } from "../db/entities/messageLog"; +import { EventBusInstance } from "./eventBus.service"; +import { OrganisationModel } from "../db/entities/organisation"; +import { Farmer, User } from "../../../common-types/src"; export declare interface MessagingInterface { on(event: 'onMessage', listener: (message: MessageLog) => void): this; @@ -30,7 +30,7 @@ export abstract class MessagingInterface extends EventEmitt * @param coopManager CoopManager we're sending a message to. * @param message The string message we want to send. */ - abstract sendMessageToCoopManager(coopManager: CoopManager, message: string): Promise; + abstract sendMessageToCoopManager(coopManager: User, message: string): Promise; /** * Send a message to an arbitrary destination. This is a way of giving flexibility @@ -67,15 +67,13 @@ export abstract class MessagingInterface extends EventEmitt throw new Error("Farmer from MessageLog not Found!"); } - for (const org_id of farmer.coopOrganisations) { - OrganisationModel.findById(org_id).then(org => { + OrganisationModel.findById(farmer.organisation).then(org => { if (org === null) { throw new Error("Org in Farmer not found!"); } EventBusInstance.publishMessage(org, message); }) - } } -} \ No newline at end of file +} diff --git a/backend/src/integrations/smsSync/smsSync.service.ts b/backend/src/integrations/smsSync/smsSync.service.ts index f2e516ecb..5fbdcd32d 100644 --- a/backend/src/integrations/smsSync/smsSync.service.ts +++ b/backend/src/integrations/smsSync/smsSync.service.ts @@ -1,9 +1,9 @@ -import { CoopManager } from "../../db/entities/coopManager"; -import { Farmer, FarmerModel } from "../../db/entities/farmer"; +import { FarmerModel } from "../../db/entities/farmer"; import { MessagingInterface } from "../messagingInterface"; import { v4 as uuidv4 } from "uuid"; -import { MessageLog, MessageLogModel, Source, Status } from "./../../db/entities/messageLog"; +import { MessageLog, MessageLogModel, Source, Status } from "../../db/entities/messageLog"; +import { Farmer, User } from "../../../../common-types/src"; export interface SMSSyncMessage { to: string; @@ -43,12 +43,16 @@ export class SMSSyncAPI extends MessagingInterface private pendingMessages: SMSSyncMessage[] = []; async sendMessageToFarmer(farmer: Farmer, message: string): Promise { - const number = farmer.mobile; - if (message === undefined || message === null || message === "") { throw new Error("Message is empty!"); } + if (farmer.mobile.length === 0) { + throw new Error("Farmer has no mobile numbers: " + farmer); + } + + const number = farmer.mobile[0]; + const messageRef = await this.sendMessage(number, message); const messageLogEntry: MessageLog = { @@ -61,12 +65,10 @@ export class SMSSyncAPI extends MessagingInterface messageRef: messageRef } - const messageLog = await MessageLogModel.create(messageLogEntry); - - return messageLog; + return await MessageLogModel.create(messageLogEntry); } - async sendMessageToCoopManager(coopManager: CoopManager, message: string): Promise { + async sendMessageToCoopManager(coopManager: User, message: string): Promise { throw new Error("Method not implemented."); // const number = coopManager.mobile; @@ -124,7 +126,7 @@ export class SMSSyncAPI extends MessagingInterface const messageLog = await MessageLogModel.create(messageLogEntry); this.emit("onMessage", messageLog); - this.notify(messageLog) + await this.notify(messageLog) return messageLog; } diff --git a/backend/src/integrations/twilio/twilio.service.ts b/backend/src/integrations/twilio/twilio.service.ts index d835a0556..0bd083041 100644 --- a/backend/src/integrations/twilio/twilio.service.ts +++ b/backend/src/integrations/twilio/twilio.service.ts @@ -1,8 +1,8 @@ -import twilio, { Twilio} from "twilio"; -import { CoopManager } from "./../../db/entities/coopManager"; -import { Farmer, FarmerModel } from "./../../db/entities/farmer"; -import { MessageLog, MessageLogModel, Source, Status } from "./../../db/entities/messageLog"; -import { MessagingInterface } from "./../../integrations/messagingInterface"; +import { Farmer, User } from "../../../../common-types/src"; +import twilio, { Twilio } from "twilio"; +import { FarmerModel } from "../../db/entities/farmer"; +import { MessageLog, MessageLogModel, Source, Status } from "../../db/entities/messageLog"; +import { MessagingInterface } from "../messagingInterface"; /** * The Message we get from Twilio on our webhook @@ -25,25 +25,29 @@ export interface TwilioMessage { * This class handles interfacing with twilio. * It provides one */ -class TwilioAPI extends MessagingInterface { +export class TwilioAPI extends MessagingInterface { client: Twilio; messagingServiceSid: string; + twilioInstance: TwilioAPI; constructor() { super(); const accountSid = process.env.Twilio_accountSid; const authToken = process.env.Twilio_token; this.messagingServiceSid = process.env.Twilio_messaging_service as string; this.client = twilio(accountSid, authToken); + this.twilioInstance = new TwilioAPI(); } async sendMessageToFarmer(farmer: Farmer, message: string): Promise { - const number = farmer.mobile; - if (message === undefined || message === null || message === "") { throw new Error("Message is empty!"); } + if (farmer.mobile.length === 0) { + throw new Error("Farmer has no mobile numbers: " + farmer); + } + const number = farmer.mobile[0]; const messageRef = await this.sendMessage(number, message); const messageLogEntry: MessageLog = { @@ -56,12 +60,10 @@ class TwilioAPI extends MessagingInterface { messageRef } - const messageLog = await MessageLogModel.create(messageLogEntry); - - return messageLog; + return await MessageLogModel.create(messageLogEntry); } - async sendMessageToCoopManager(coopManager: CoopManager, message: string): Promise { + async sendMessageToCoopManager(coopManager: User, message: string): Promise { throw new Error("Method not implemented."); } @@ -112,4 +114,4 @@ class TwilioAPI extends MessagingInterface { } -export const TwilioInstance = new TwilioAPI(); + diff --git a/backend/src/integrations/weather-company-api.service.ts b/backend/src/integrations/weather-company-api.service.ts deleted file mode 100644 index c1c878b88..000000000 --- a/backend/src/integrations/weather-company-api.service.ts +++ /dev/null @@ -1,64 +0,0 @@ -import axios from "axios"; -import { GeoCode, Languages, Units, CommonOptions, Formats } from "./weather-company-api.types"; - -const testForecastData = {"calendarDayTemperatureMax":[21,22,24,24,24,24,24,24,25,25,25,25,24,25,25],"calendarDayTemperatureMin":[18,17,17,17,16,17,17,17,17,17,17,17,17,17,17],"dayOfWeek":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],"expirationTimeUtc":[1645343951,1645343951,1645343951,1645343951,1645343951,1645343951,1645343951,1645343951,1645343951,1645343951,1645343951,1645343951,1645343951,1645343951,1645343951],"moonPhase":["Waning Gibbous","Waning Gibbous","Waning Gibbous","Waning Gibbous","Last Quarter","Waning Crescent","Waning Crescent","Waning Crescent","Waning Crescent","Waning Crescent","New","Waxing Crescent","Waxing Crescent","Waxing Crescent","Waxing Crescent"],"moonPhaseCode":["WNG","WNG","WNG","WNG","LQ","WNC","WNC","WNC","WNC","WNC","N","WXC","WXC","WXC","WXC"],"moonPhaseDay":[18,19,20,21,22,24,25,26,27,28,29,1,2,3,3],"moonriseTimeLocal":["2022-02-20T21:05:50+0200","2022-02-21T21:47:27+0200","2022-02-22T22:31:47+0200","2022-02-23T23:21:30+0200","","2022-02-25T00:15:43+0200","2022-02-26T01:15:50+0200","2022-02-27T02:18:43+0200","2022-02-28T03:23:17+0200","2022-03-01T04:25:19+0200","2022-03-02T05:25:08+0200","2022-03-03T06:20:36+0200","2022-03-04T07:13:59+0200","2022-03-05T08:04:43+0200","2022-03-06T08:54:39+0200"],"moonriseTimeUtc":[1645383950,1645472847,1645561907,1645651290,null,1645740943,1645830950,1645921123,1646011397,1646101519,1646191508,1646281236,1646370839,1646460283,1646549679],"moonsetTimeLocal":["2022-02-20T08:50:23+0200","2022-02-21T09:43:37+0200","2022-02-22T10:38:23+0200","2022-02-23T11:37:00+0200","2022-02-24T12:37:49+0200","2022-02-25T13:41:32+0200","2022-02-26T14:44:20+0200","2022-02-27T15:45:07+0200","2022-02-28T16:40:27+0200","2022-03-01T17:31:14+0200","2022-03-02T18:16:18+0200","2022-03-03T18:58:16+0200","2022-03-04T19:37:52+0200","2022-03-05T20:15:34+0200","2022-03-06T20:53:52+0200"],"moonsetTimeUtc":[1645339823,1645429417,1645519103,1645609020,1645699069,1645789292,1645879460,1645969507,1646059227,1646148674,1646237778,1646326696,1646415472,1646504134,1646592832],"narrative":["Thunderstorms developing in the afternoon. Highs 20 to 22ºC and lows 16 to 18ºC.","Thunderstorms. Highs 21 to 23ºC and lows 16 to 18ºC.","Scattered thunderstorms. Highs 23 to 25ºC and lows 16 to 18ºC.","Thunderstorms developing in the afternoon. Highs 23 to 25ºC and lows 15 to 17ºC.","Thunderstorms. Highs 23 to 25ºC and lows 16 to 18ºC.","Thunderstorms. Highs 23 to 25ºC and lows 16 to 18ºC.","Scattered thunderstorms. Highs 23 to 25ºC and lows 16 to 18ºC.","Scattered thunderstorms. Highs 23 to 25ºC and lows 16 to 18ºC.","Scattered thunderstorms. Highs 24 to 26ºC and lows 16 to 18ºC.","Scattered thunderstorms. Highs 24 to 26ºC and lows 16 to 18ºC.","Scattered thunderstorms. Highs 24 to 26ºC and lows 16 to 18ºC.","Scattered thunderstorms. Highs 24 to 26ºC and lows 16 to 18ºC.","Thunderstorms. Highs 23 to 25ºC and lows 16 to 18ºC.","Thunderstorms. Highs 24 to 26ºC and lows 16 to 18ºC.","Scattered thunderstorms. Highs 24 to 26ºC and lows 15 to 17ºC."],"qpf":[1.64,8,3.47,1.64,7.29,5.77,3.96,3.2,1.8,4.84,4,4.2,4.77,5.43,4.05],"qpfSnow":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"sunriseTimeLocal":["2022-02-20T05:48:00+0200","2022-02-21T05:48:16+0200","2022-02-22T05:48:31+0200","2022-02-23T05:48:45+0200","2022-02-24T05:48:59+0200","2022-02-25T05:49:13+0200","2022-02-26T05:49:26+0200","2022-02-27T05:49:38+0200","2022-02-28T05:49:50+0200","2022-03-01T05:50:02+0200","2022-03-02T05:50:13+0200","2022-03-03T05:50:24+0200","2022-03-04T05:50:34+0200","2022-03-05T05:50:44+0200","2022-03-06T05:50:53+0200"],"sunriseTimeUtc":[1645328880,1645415296,1645501711,1645588125,1645674539,1645760953,1645847366,1645933778,1646020190,1646106602,1646193013,1646279424,1646365834,1646452244,1646538653],"sunsetTimeLocal":["2022-02-20T18:16:07+0200","2022-02-21T18:15:38+0200","2022-02-22T18:15:08+0200","2022-02-23T18:14:38+0200","2022-02-24T18:14:06+0200","2022-02-25T18:13:35+0200","2022-02-26T18:13:02+0200","2022-02-27T18:12:29+0200","2022-02-28T18:11:56+0200","2022-03-01T18:11:21+0200","2022-03-02T18:10:47+0200","2022-03-03T18:10:11+0200","2022-03-04T18:09:36+0200","2022-03-05T18:09:00+0200","2022-03-06T18:08:23+0200"],"sunsetTimeUtc":[1645373767,1645460138,1645546508,1645632878,1645719246,1645805615,1645891982,1645978349,1646064716,1646151081,1646237447,1646323811,1646410176,1646496540,1646582903],"temperatureMax":[21,22,24,24,24,24,24,24,25,25,25,25,24,25,25],"temperatureMin":[17,17,17,16,17,17,17,17,17,17,17,17,17,17,16],"validTimeLocal":["2022-02-20T07:00:00+0200","2022-02-21T07:00:00+0200","2022-02-22T07:00:00+0200","2022-02-23T07:00:00+0200","2022-02-24T07:00:00+0200","2022-02-25T07:00:00+0200","2022-02-26T07:00:00+0200","2022-02-27T07:00:00+0200","2022-02-28T07:00:00+0200","2022-03-01T07:00:00+0200","2022-03-02T07:00:00+0200","2022-03-03T07:00:00+0200","2022-03-04T07:00:00+0200","2022-03-05T07:00:00+0200","2022-03-06T07:00:00+0200"],"validTimeUtc":[1645333200,1645419600,1645506000,1645592400,1645678800,1645765200,1645851600,1645938000,1646024400,1646110800,1646197200,1646283600,1646370000,1646456400,1646542800],"daypart":[{"cloudCover":[94,92,83,84,73,61,57,59,67,66,68,69,59,68,64,64,49,62,52,72,57,68,56,53,66,52,68,70,60,62],"dayOrNight":["D","N","D","N","D","N","D","N","D","N","D","N","D","N","D","N","D","N","D","N","D","N","D","N","D","N","D","N","D","N"],"daypartName":["Today","Tonight","Tomorrow","Tomorrow night","Tuesday","Tuesday night","Wednesday","Wednesday night","Thursday","Thursday night","Friday","Friday night","Saturday","Saturday night","Sunday","Sunday night","Monday","Monday night","Tuesday","Tuesday night","Wednesday","Wednesday night","Thursday","Thursday night","Friday","Friday night","Saturday","Saturday night","Sunday","Sunday night"],"iconCode":[38,11,4,47,38,47,38,47,4,47,4,47,38,47,38,29,38,29,38,47,38,47,38,47,4,4,4,4,38,11],"iconCodeExtend":[7203,1140,400,3809,3800,6200,7203,3809,400,3809,400,3809,3800,3809,3800,2900,3800,2900,3800,3809,3800,3809,3800,3809,400,400,400,400,3800,1100],"narrative":["Thunderstorms developing in the afternoon. High 21ºC. Winds WSW at 10 to 15 km/h. Chance of rain 40%.","Thundershowers. Low 17ºC. Winds WSW and variable. Chance of rain 40%.","Thunderstorms. High 22ºC. Winds SW at 10 to 15 km/h. Chance of rain 80%.","Scattered thunderstorms. Low 17ºC. Winds SW and variable. Chance of rain 50%.","Scattered thunderstorms. High 24ºC. Winds S at 10 to 15 km/h. Chance of rain 50%.","Thunderstorms early. Low 17ºC. Winds S and variable. Chance of rain 40%.","Thunderstorms developing in the afternoon. High 24ºC. Winds S at 10 to 15 km/h. Chance of rain 50%.","Scattered thunderstorms. Low 16ºC. Winds SSE and variable. Chance of rain 40%.","Thunderstorms. High 24ºC. Winds S and variable. Chance of rain 70%.","Scattered thunderstorms. Low 17ºC. Winds NW and variable. Chance of rain 60%.","Thunderstorms. High 24ºC. Winds WNW and variable. Chance of rain 70%.","Scattered thunderstorms. Low 17ºC. Winds SW and variable. Chance of rain 60%.","Scattered thunderstorms. High 24ºC. Winds SSW at 10 to 15 km/h. Chance of rain 50%.","Scattered thunderstorms. Low 17ºC. Winds SE and variable. Chance of rain 50%.","Scattered thunderstorms. High 24ºC. Winds SSE at 10 to 15 km/h. Chance of rain 50%.","Partly cloudy. Low 17ºC. Winds SE and variable.","Scattered thunderstorms. High 25ºC. Winds SSE at 10 to 15 km/h. Chance of rain 40%.","Partly cloudy. Low 17ºC. Winds ESE and variable.","Scattered thunderstorms. High 25ºC. Winds SE at 10 to 15 km/h. Chance of rain 60%.","Scattered thunderstorms. Low 17ºC. Winds SE and variable. Chance of rain 50%.","Scattered thunderstorms. High 25ºC. Winds SSE at 10 to 15 km/h. Chance of rain 50%.","Scattered thunderstorms. Low 17ºC. Winds SSE and variable. Chance of rain 50%.","Scattered thunderstorms. High 25ºC. Winds SSE at 10 to 15 km/h. Chance of rain 50%.","Scattered thunderstorms. Low 17ºC. Winds SSE and variable. Chance of rain 50%.","Thunderstorms. High 24ºC. Winds SSE at 10 to 15 km/h. Chance of rain 60%.","Thunderstorms. Low 17ºC. Winds SSE and variable. Chance of rain 60%.","Thunderstorms. High 25ºC. Winds SSE at 10 to 15 km/h. Chance of rain 60%.","Thunderstorms. Low 17ºC. Winds SSE and variable. Chance of rain 60%.","Scattered thunderstorms. High 25ºC. Winds SSE at 10 to 15 km/h. Chance of rain 50%.","Showers. Low 16ºC. Winds SSE and variable. Chance of rain 50%."],"precipChance":[40,42,78,47,51,43,45,42,68,55,74,58,53,53,54,24,44,24,57,51,53,51,50,45,60,60,60,60,51,50],"precipType":["rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain","rain"],"qpf":[0.3,1.34,6.46,1.54,2.6,0.86,0.84,0.8,4.73,2.56,4.5,1.26,2.76,1.2,3,0,1.56,0,3.3,1.54,2.6,1.4,2.9,1.3,3.24,1.53,4.03,1.4,2.9,1.15],"qpfSnow":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"qualifierCode":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],"qualifierPhrase":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],"relativeHumidity":[90,96,87,94,79,95,78,94,81,94,80,95,80,96,79,96,76,94,75,93,74,93,75,94,77,94,77,94,77,96],"snowRange":["","","","","","","","","","","","","","","","","","","","","","","","","","","","","",""],"temperature":[21,17,22,17,24,17,24,16,24,17,24,17,24,17,24,17,25,17,25,17,25,17,25,17,24,17,25,17,25,16],"temperatureHeatIndex":[22,20,23,21,25,21,24,20,24,21,24,21,24,21,25,21,27,21,27,21,26,21,25,21,25,21,25,21,25,20],"temperatureWindChill":[20,18,18,18,19,17,18,17,18,17,18,17,18,17,18,17,18,17,18,17,19,17,18,17,18,17,18,17,18,17],"thunderCategory":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],"thunderIndex":[2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,0,2,0,2,2,2,2,2,2,2,2,2,2,2,0],"uvDescription":["High","Low","Very High","Low","Extreme","Low","Extreme","Low","Extreme","Low","Extreme","Low","Extreme","Low","Extreme","Low","Extreme","Low","Extreme","Low","Extreme","Low","Extreme","Low","Extreme","Low","Extreme","Low","Extreme","Low"],"uvIndex":[7,0,10,0,11,0,11,0,11,0,11,0,11,0,11,0,11,0,11,0,11,0,11,0,11,0,11,0,11,0],"windDirection":[240,250,232,228,189,183,171,157,171,326,295,229,195,145,163,139,156,113,146,124,150,149,156,162,158,155,150,154,152,160],"windDirectionCardinal":["WSW","WSW","SW","SW","S","S","S","SSE","S","NW","WNW","SW","SSW","SE","SSE","SE","SSE","ESE","SE","SE","SSE","SSE","SSE","SSE","SSE","SSE","SSE","SSE","SSE","SSE"],"windPhrase":["Winds WSW at 10 to 15 km/h.","Winds WSW and variable.","Winds SW at 10 to 15 km/h.","Winds SW and variable.","Winds S at 10 to 15 km/h.","Winds S and variable.","Winds S at 10 to 15 km/h.","Winds SSE and variable.","Winds S and variable.","Winds NW and variable.","Winds WNW and variable.","Winds SW and variable.","Winds SSW at 10 to 15 km/h.","Winds SE and variable.","Winds SSE at 10 to 15 km/h.","Winds SE and variable.","Winds SSE at 10 to 15 km/h.","Winds ESE and variable.","Winds SE at 10 to 15 km/h.","Winds SE and variable.","Winds SSE at 10 to 15 km/h.","Winds SSE and variable.","Winds SSE at 10 to 15 km/h.","Winds SSE and variable.","Winds SSE at 10 to 15 km/h.","Winds SSE and variable.","Winds SSE at 10 to 15 km/h.","Winds SSE and variable.","Winds SSE at 10 to 15 km/h.","Winds SSE and variable."],"windSpeed":[12,7,14,6,15,8,14,9,9,5,10,5,12,7,11,6,10,7,10,6,10,7,12,7,12,7,11,7,12,7],"wxPhraseLong":["PM T-Storms","T-Showers","T-Storms","Scattered T-Storms","Scattered T-Storms","T-Storms Early","PM T-Storms","Scattered T-Storms","T-Storms","Scattered T-Storms","T-Storms","Scattered T-Storms","Scattered T-Storms","Scattered T-Storms","Scattered T-Storms","Partly Cloudy","Scattered T-Storms","Partly Cloudy","Scattered T-Storms","Scattered T-Storms","Scattered T-Storms","Scattered T-Storms","Scattered T-Storms","Scattered T-Storms","T-Storms","T-Storms","T-Storms","T-Storms","Scattered T-Storms","Showers"],"wxPhraseShort":["","","","","","","","","","","","","","","","","","","","","","","","","","","","","",""]}]} - -const apiRequestLimit = 50; // It's really 100 per minute -let apiRequestCounter = 0; - -export class WeatherCompanyAPI { - - defaultOptions: CommonOptions; - - baseAPI = "https://api.weather.com/" - - constructor(private apiKey = process.env.weather_company_apiKey as string, private unit = Units.metric, private language = Languages.English, private format = Formats.JSON) { - if (apiKey == undefined) { - console.error("Weather Company API isn't defined!"); - console.error("Please set it on the 'weather_company_apiKey' env variable or pass it to the constructor"); - throw new Error("Please pass an API key to the Weather API"); - } - - this.defaultOptions = { - format: Formats.JSON, - language, - units: unit - } - - // setup API throttler, every minute it resets the apiRequestLimit - setInterval(() => { - apiRequestCounter = 0; - }, 1000 * 60); - } - - GeoCodeToString(geocode: GeoCode) { - return `${geocode.latitude},${geocode.longitude}` - } - - apiHitCounter() { - if (apiRequestCounter >= apiRequestLimit) { - throw new Error("Too many Requests"); - } - else { - apiRequestCounter++; - } - } - - async daily15DayForecast(geocode: GeoCode, commonOptions = this.defaultOptions) { - const paramOptions = { - geocode: this.GeoCodeToString(geocode), - apiKey: this.apiKey - } - const queryOptions = Object.assign({}, commonOptions, paramOptions); - - this.apiHitCounter() - - const response = await axios.get(this.baseAPI + "v3/wx/forecast/daily/15day", { - params: queryOptions - }); - - return response.data; - - // return testForecastData; - } -} diff --git a/backend/src/integrations/weatherCompany/WeatherCompanyService.ts b/backend/src/integrations/weatherCompany/WeatherCompanyService.ts new file mode 100644 index 000000000..1b993c73c --- /dev/null +++ b/backend/src/integrations/weatherCompany/WeatherCompanyService.ts @@ -0,0 +1,53 @@ +import axios from "axios"; +import { CommonOptions, DataFormat, GeoCodeNumber, geoCodeToString, Language, Unit, WeatherCompanyConfig } from "../../../../common-types/src"; + +const apiRequestLimit = 50; // It's really 100 per minute +let apiRequestCounter = 0; + +class WeatherCompanyService { + + defaultOptions: CommonOptions; + + // baseAPI = "https://api.weather.com/" + + constructor() { + this.defaultOptions = { + format: DataFormat.JSON, + language: Language.English, + units: Unit.metric + } + + // setup API throttler, every minute it resets the apiRequestLimit + setInterval(() => { + apiRequestCounter = 0; + }, 1000 * 60); + } + + apiHitCounter() { + if (apiRequestCounter >= apiRequestLimit) { + throw new Error("Too many Requests"); + } + else { + apiRequestCounter++; + } + } + + async daily15DayForecast(config: WeatherCompanyConfig, geocode: GeoCodeNumber) { + const commonOptions = config.options || this.defaultOptions; + const paramOptions = { + geocode: geoCodeToString(geocode), + apiKey: config.apiKey + } + const queryOptions = Object.assign({}, commonOptions, paramOptions); + + this.apiHitCounter() + + const response = await axios.get(config.apiUrl + "v3/wx/forecast/daily/15day", { + params: queryOptions + }); + + return response.data; + } +} + +export const weatherCompanyService: WeatherCompanyService = new WeatherCompanyService(); diff --git a/backend/src/main.ts b/backend/src/main.ts index c7e67f531..05358adc8 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -12,23 +12,23 @@ import 'dotenv/config'; import { mongoInit } from "./db/mongodb"; import farmerRoutes from "./routes/farmer-route"; -import lotRoutes from "./routes/lot-route"; +// import lotRoutes from "./routes/lot-route"; import cropRoutes from "./routes/crop-route"; import dashboardRoutes from "./routes/dashboard-route"; import recommendationsRoutes from "./routes/recommendations-route"; import weatherRoutes from "./routes/weather-route"; -import coopManagerRoutes from "./routes/coopManager-route"; +import userRoutes from "./routes/user-route"; import organisationRoutes from "./routes/organisation-route"; import messageLogRoutes from "./routes/messaging-route"; import smsRoutes from "./routes/sms-route"; import foodTrustRoutes from "./routes/food-trust-route"; -import { formatUser, ensureAuthenticated } from "./auth/helpers"; +import { ensureAuthenticated, formatUser } from "./auth/helpers"; import { IBMidStrategy } from "./auth/IBMiDStrategy"; -import { SocketIOManager, SocketIOManagerInstance } from "./sockets/socket.io"; +import { SocketIOManagerInstance } from "./sockets/socket.io"; import { Server } from "http"; -mongoInit(); +mongoInit().then(() => console.log("Connected to DB")); const app = express(); @@ -50,11 +50,14 @@ app.use(session({ app.use(passport.initialize()); app.use(passport.session()); +// @ts-ignore passport.serializeUser(function(user, done) { done(null, user); }); +// @ts-ignore passport.deserializeUser(function(obj, done) { + // @ts-ignore done(null, obj); }); @@ -63,9 +66,9 @@ passport.use(IBMidStrategy); app.get('/login', passport.authenticate('openidconnect', { state: Math.random().toString(36).substr(2, 10) })); app.get('/auth/sso/callback', function (req, res, next) { - // @ts-ignore - let redirect_url = "/app"; + let redirect_url; if (process.env.NODE_ENV == "production") { + // @ts-ignore redirect_url = req.session.originalUrl; redirect_url = "https://openharvest.net/"; } @@ -86,11 +89,13 @@ app.get('/failure', function(req, res) { app.get('/hello', ensureAuthenticated, function (req, res) { - var claims = req.user['_json']; - var html ="

Hello " + claims.given_name + " " + claims.family_name + ":

"; + // @ts-ignore + const claims = req.user['_json']; + let html ="

Hello " + claims.given_name + " " + claims.family_name + ":

"; html += "User details (ID token in _json object):

"; + // @ts-ignore html += "
" + JSON.stringify(req.user, null, 4) + "
"; html += "
logout"; @@ -105,6 +110,7 @@ app.get('/hello', ensureAuthenticated, function (req, res) { app.get('/me', ensureAuthenticated, (req, res) => { + // @ts-ignore return res.json(formatUser(req.user)); }); @@ -113,14 +119,14 @@ app.get('/me', ensureAuthenticated, (req, res) => { // app.use('/api/names', nameRoutes); app.use("/api/farmer", farmerRoutes); -app.use("/api/lot", lotRoutes); +// app.use("/api/lot", lotRoutes); app.use("/api/crop", cropRoutes); app.use("/api/dashboard", dashboardRoutes); app.use("/api/recommendations", recommendationsRoutes); app.use("/api/weather", weatherRoutes); app.use("/api/foodtrust", foodTrustRoutes) -app.use("/api/coopManager", coopManagerRoutes); +app.use("/api/coopManager", userRoutes); app.use("/api/organisation", organisationRoutes); app.use("/api/messaging", messageLogRoutes); app.use("/api/sms", smsRoutes); @@ -151,8 +157,7 @@ if (process.env.NODE_ENV == "production") { console.log("Server starting on http://localhost:" + port); }); -} -else { +} else { const sslKey = process.env['SSL_Key']; const sslCert = process.env['SSL_Cert']; if (sslKey == undefined || sslCert == undefined) { diff --git a/backend/src/routes/coopManager-route.ts b/backend/src/routes/coopManager-route.ts deleted file mode 100644 index 4253813bc..000000000 --- a/backend/src/routes/coopManager-route.ts +++ /dev/null @@ -1,90 +0,0 @@ -// import dependencies and initialize the express router -import { Router } from "express"; -import { getOrganisations } from "./../services/organisation.service"; -import { addCoopManagerToOrganisation, doesUserExist, getCoopManager, onBoardUser } from "./../services/coopManager.service"; - -const router = Router(); - -// define routes -router.get(":id", async (req, res) => { - const id = req.params.id; - const manager = await getCoopManager(id); - if (manager !== null) { - return res.json(manager.toObject()); - } - else { - return res.status(404).end(); - } -}); - -router.get("/hasBeenOnBoarded", async (req, res) => { - const prefix = "IBMid:"; - if (req.user === undefined) { - return res.status(400).send("User hasn't logged in."); - } - const id = `${prefix}${req.user.id}`; - const result = await doesUserExist(id); - res.json({exists: result}); -}); - -router.post("/onboard", async (req, res) => { - if (req.body === undefined) { - return res.status(400).send("Body is missing"); - } - if (req.body.oAuthSource === undefined) { - return res.status(400).send("oAuthSource is missing"); - } - if (req.body.oAuthId === undefined) { - return res.status(400).send("oAuthId is missing"); - } - if (req.body.user === undefined) { - return res.status(400).send("user (Coop Manager) is missing"); - } - - const userDoc = await onBoardUser(req.body.oAuthSource, req.body.oAuthId, req.body.user); - - // Set the Organisation variables on the user - req.user.isOnboarded = true; - req.user.coopManager = userDoc.toObject(); - req.user.organisations = await getOrganisations(userDoc.coopOrganisations); - req.user.selectedOrganisation = req.user.organisations[0]; - - res.json(userDoc.toObject()); -}); - -router.put("/setCurrentOrganisation", async (req, res) => { - if (req.body === undefined) { - return res.status(400).send("Body is missing"); - } - if (req.body.orgId === undefined) { - return res.status(400).send("orgId is missing"); - } - const orgId = req.body.orgId; - const org = req.user.organisations.find(it => it._id == orgId); - if (org == undefined) { - return res.status(400).json("User is not part of organisation"); - } - - req.user.selectedOrganisation = org; - - res.json(org); -}) - -router.put("/:id/addOrganisation", async (req, res) => { - if (req.body === undefined) { - return res.status(400).send("Body is missing"); - } - if (req.body.orgId === undefined) { - return res.status(400).send("coopManagerId is missing"); - } - const orgId = req.body.orgId; - const coopManagerId = req.params.id; - const coopUser = await addCoopManagerToOrganisation(coopManagerId, orgId); - - req.user.coopManager = coopUser.toObject(); - req.user.organisations = await getOrganisations(coopUser.coopOrganisations, true); - - res.json(coopUser.toObject()); -}); - -export default router; diff --git a/backend/src/routes/crop-route.ts b/backend/src/routes/crop-route.ts index 0b6ad622a..5fd471e92 100644 --- a/backend/src/routes/crop-route.ts +++ b/backend/src/routes/crop-route.ts @@ -1,7 +1,7 @@ import { Request, Response, Router } from "express"; -import CropService from "../services/crop.service"; -var router = Router(); -const cropService = new CropService(); +import { cropService } from "../services/CropService"; + +const router = Router(); router.get("/", getAllCrops); diff --git a/backend/src/routes/dashboard-route.ts b/backend/src/routes/dashboard-route.ts index c5c00f016..d202ed157 100644 --- a/backend/src/routes/dashboard-route.ts +++ b/backend/src/routes/dashboard-route.ts @@ -1,7 +1,8 @@ import { Router } from "express"; +import LandAreasService from "../services/land-areas.service"; + var router = Router(); -import LandAreasService from "../services/land-areas.service"; const lotAreas = new LandAreasService(); //data table diff --git a/backend/src/routes/farmer-route.ts b/backend/src/routes/farmer-route.ts index 35ee77091..e4acea426 100644 --- a/backend/src/routes/farmer-route.ts +++ b/backend/src/routes/farmer-route.ts @@ -1,27 +1,18 @@ -import { Router, Request, Response } from "express"; -import { EISField } from "../integrations/EIS/EIS.types"; -import { EISAPIService } from "../integrations/EIS/EIS-api.service"; -import { Farmer, FarmerModel } from "../db/entities/farmer"; -import LandAreasService from "../services/land-areas.service"; +import { Request, Response, Router } from "express"; +import { farmerService } from "../services/FarmerService"; + +import { FarmerModel } from "../db/entities/farmer"; +import { farmService } from "../services/FarmService"; +import { Farmer, isUndefined, NewFarmer } from "../../../common-types/src"; // const LotAreaService = require("./../services/lot-areas.service"); // const lotAreas = new LandAreasService(); -const EISKey = process.env.EIS_apiKey; - -if (EISKey == undefined) { - console.error("You must define 'EIS_apiKey' in the environment!"); - process.exit(-1); -} - -const eisAPIService = new EISAPIService(EISKey); - const router = Router(); router.get("/", async (req: Request, res: Response) => { try { - const docs = await FarmerModel.find().lean().exec(); - res.json(docs); + res.json(farmerService.getFarmers()); } catch (e) { console.error(e); res.status(500).json(e); @@ -35,9 +26,7 @@ async function createOrUpdateFarmer(req: Request, res: Response) { return; } try { - const farmerDoc = new FarmerModel(farmer); - const updatedDoc = farmerDoc.save(); - res.json(updatedDoc); + res.json(farmerService.saveFarmer(farmer)); } catch (e) { console.error(e); res.status(500).json(e); @@ -45,17 +34,8 @@ async function createOrUpdateFarmer(req: Request, res: Response) { } -async function getFarmer(id: string) { - // Aggregate with land areas eventually - const farmer = await FarmerModel.findById(id).lean().exec(); - if (farmer == null) { - return null; - } - - // Get Fields - const field = await eisAPIService.getFarmerField(id); - - return farmer; +async function getFarmer(id: string): Promise { + return farmerService.getFarmer(id); } router.post("/", createOrUpdateFarmer); @@ -70,7 +50,7 @@ router.get("/:id", async (req: Request, res: Response) => { } try { - const farmer = getFarmer(id); + const farmer = await getFarmer(id); if (farmer == null) { res.status(404).end(); } @@ -92,7 +72,7 @@ router.delete("/:id", async(req: Request, res: Response) => { return; } try { - const result = await FarmerModel.deleteOne({_id: id}); + const result = await FarmerModel.findByIdAndDelete(id); res.json(result); } catch (e) { console.error(e); @@ -100,48 +80,23 @@ router.delete("/:id", async(req: Request, res: Response) => { } }); -export interface FarmerAddDTO { - farmer: Farmer; - field: EISField -} -router.post("/add", async(req: Request, res: Response) => { - const {farmer, field}: FarmerAddDTO = req.body; - if (farmer == undefined) { - res.status(400).send("Farmer not defined"); + +router.post("/add", async(req: Request<{}, {}, NewFarmer>, res: Response) => { + const newFarmer: NewFarmer = req.body; + if (isUndefined(newFarmer)) { + res.status(400).send("Farmer is not defined"); return; } - if (field == undefined) { - res.status(400).send("Field not defined"); + if (isUndefined(newFarmer.farms)) { + res.status(400).send("Farm is not defined"); return; } - // First we'll create the farmer - const farmerDoc = new FarmerModel(farmer); - const newFarmer = await farmerDoc.save(); - - if (newFarmer._id == undefined) { - throw new Error("Farmer ID is not defined after saving!") - } - - // Then we'll create the Field - - // We have to set the farmer ID on the field first - for (let i = 0; i < field.subFields.length; i++) { - const properties = field.subFields[i].geo.geojson.features[0].properties; - properties.open_harvest_farmer_id = newFarmer._id!!.toString(); - properties.open_harvest.farmer_id = newFarmer._id!!.toString(); - } - - const createdFieldsUuids = await eisAPIService.createField(field); - const fieldUuid = createdFieldsUuids.field; - - const createdField = await eisAPIService.getField(fieldUuid); - - const farmerObj = newFarmer.toObject(); + const farmer = await farmerService.saveFarmer(newFarmer); - farmerObj.field = createdField; + await farmService.saveFarms(farmer, newFarmer.farms); - res.json(farmerObj); + res.json(farmer); }); // // Link Lot diff --git a/backend/src/routes/lot-route.ts b/backend/src/routes/lot-route.ts deleted file mode 100644 index 7320379f3..000000000 --- a/backend/src/routes/lot-route.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Router, Request, Response } from "express"; -var router = Router(); - -import LandAreasService from "../services/land-areas.service"; -const lotAreas = new LandAreasService(); - -router.get("/", getAllLots); -router.get("/:id", getLot); -router.get("/inBbox/:bboxString", getAreaInBox); -router.put("/", updateLot); - -async function getAllLots(req: Request, res: Response) { - try { - const response = await lotAreas.getAllLots(); - res.json(response); - } catch (e) { - console.error(e); - res.status(500).json(e); - } -} - -async function getLot(req: Request, res: Response) { - const id = req.params["id"]; - if (!id) { - res.sendStatus(400).end(); - return; - } - try { - const lot = await lotAreas.getLot(id); - res.json(lot); - } catch (e) { - console.error(e); - res.status(500).json(e); - } -} - -async function updateLot(req: Request, res: Response) { - const lot = req.body; - if (!lot) { - res.sendStatus(400).end(); - return; - } - try { - const newLot = await lotAreas.updateLot(lot); - res.json(newLot); - } catch (e) { - console.error(e); - res.status(500).json(e); - } -} - -async function getAreaInBox(req: Request, res: Response) { - const bboxStr = req.params["bboxString"]; - const elems = bboxStr.split(","); - const bbox = { - lowerLeft: { - lat: elems[0], - lng: elems[1], - }, - upperRight: { - lat: elems[2], - lng: elems[3], - }, - }; - - try { - const response = await lotAreas.getAreasInBbox(bbox); - res.json(response); - } catch (e) { - console.error(e); - res.status(500).json(e); - } -} - -export default router; diff --git a/backend/src/routes/messaging-route.ts b/backend/src/routes/messaging-route.ts index dd99e5177..bcef68094 100644 --- a/backend/src/routes/messaging-route.ts +++ b/backend/src/routes/messaging-route.ts @@ -1,6 +1,6 @@ // import dependencies and initialize the express router import { Router } from "express"; -import { TwilioInstance } from "./../integrations/twilio/twilio.service"; +import { TwilioAPI } from "../integrations/twilio/twilio.service"; import { MessageLogModel } from "../db/entities/messageLog"; import { FarmerModel } from "../db/entities/farmer"; @@ -22,7 +22,7 @@ router.post("/sendSMSToFarmer", async (req, res) => { } try { - const messageLog = await TwilioInstance.sendMessageToFarmer(farmer, message); + const messageLog = await new TwilioAPI().sendMessageToFarmer(farmer, message); res.json(messageLog) } catch (e: any) { diff --git a/backend/src/routes/organisation-route.ts b/backend/src/routes/organisation-route.ts index 63f654e5e..2c91f1563 100644 --- a/backend/src/routes/organisation-route.ts +++ b/backend/src/routes/organisation-route.ts @@ -1,53 +1,51 @@ // import dependencies and initialize the express router -import { Router } from "express"; -import { createOrganisationFromName, getAllOrganisations, getOrganisation, getOrganisations } from "./../services/organisation.service"; +import { isDefined, isUndefined, UserOrganisationDto } from "../../../common-types/src"; +import { Request, Router } from "express"; +import { organisationService } from "../services/OrganisationService"; const router = Router(); router.get("/", async (req, res) => { - const orgs = await getAllOrganisations(true); - // console.log(orgs); + const orgs = await organisationService.getAllOrganisations(true); return res.json(orgs); }); // define routes router.get("/:id", async (req, res) => { const id = req.params.id; - const org = await getOrganisation(id); + const org = await organisationService.getOrganisation(id); if (org == null) { return res.sendStatus(404); } else { - return res.json(org.toObject()); + return res.json(org); } }); -router.post("/", async (req, res) => { - if (req.body === undefined) { +router.post("/", async (req: Request<{}, {}, UserOrganisationDto>, res) => { + if (isUndefined(req.body)) { return res.status(400).send("Body is missing"); } - if (req.body.name === undefined) { + if (isUndefined(req.body.name)) { return res.status(400).send("name is missing"); } const name = req.body.name; - console.log("Creating Org:", name); - const doc = await createOrganisationFromName(name); - res.json(doc.toObject()); -}); -router.get("/my", async (req, res) => { - if (req.user == undefined) { - return res.status(401); + if (!/^[\dA-Za-z\s\-]+$/.test(name)) { + return res.status(400).send("Invalid name with not allowed characters"); } - const isOnboarded = req.user.isOnboarded; - if (!isOnboarded) { - return res.status(400).json({error: "user is not onboarded"}); + const organisation = await organisationService.getOrganisation(name) + + if (isDefined(organisation)) { + return res.status(409).send("Organisation already exists: " + name); } - // Get the organisations of the user - const orgs = await getOrganisations(req.user, true); - return orgs; + + console.log("Creating Org:", name); + const doc = await organisationService.createOrganisation(req.body); + res.json(doc); }); + export default router; diff --git a/backend/src/routes/recommendations-route.ts b/backend/src/routes/recommendations-route.ts index 11554a5cf..9ba82126c 100644 --- a/backend/src/routes/recommendations-route.ts +++ b/backend/src/routes/recommendations-route.ts @@ -1,7 +1,8 @@ import { Router } from "express"; +import RecommendationsService from "../services/recommendations.service"; + var router = Router(); -import RecommendationsService from "../services/recommendations.service"; const recommendationsService = new RecommendationsService(); router.post("/", async(req, res) => { try { diff --git a/backend/src/routes/sms-route.ts b/backend/src/routes/sms-route.ts index 89ba26a9f..0997038bd 100644 --- a/backend/src/routes/sms-route.ts +++ b/backend/src/routes/sms-route.ts @@ -4,9 +4,8 @@ import { Router } from "express"; -import { MessageLogModel } from "../db/entities/messageLog"; -import { SMSSyncAPIInstance, SMSSyncMessageReceivedFormat } from "./../integrations/smsSync/smsSync.service"; -import { TwilioInstance, TwilioMessage } from "../integrations/twilio/twilio.service"; +import { SMSSyncAPIInstance, SMSSyncMessageReceivedFormat } from "../integrations/smsSync/smsSync.service"; +import { TwilioAPI, TwilioMessage } from "../integrations/twilio/twilio.service"; const router = Router(); @@ -53,7 +52,7 @@ router.post("/twilio-sms-incoming", async (req, res) => { // console.log("Twilio Message:", req.body); const message: TwilioMessage = req.body; - TwilioInstance.onReceivedMessage(message); + new TwilioAPI().onReceivedMessage(message); res.status(200).end(); }); diff --git a/backend/src/routes/user-route.ts b/backend/src/routes/user-route.ts new file mode 100644 index 000000000..f59987c07 --- /dev/null +++ b/backend/src/routes/user-route.ts @@ -0,0 +1,41 @@ +import { Request, Router } from "express"; +import { userService } from "../services/UserService"; +import { isUndefined, UserDto } from "../../../common-types/src"; + +const router = Router(); + +// define routes +router.get(":id", async (req, res) => { + const id = req.params.id; + const users = await userService.getUser(id); + if (isUndefined(users) || users.length === 0) { + return res.status(404).end(); + } + + return res.json(users); +}); + +router.post("/hasBeenOnBoarded", async (req: Request<{}, {}, UserDto>, res) => { + if (req.body === undefined) { + return res.status(400).send("User hasn't logged in."); + } + const result = await userService.doesUserExist(req.body); + res.json({exists: result}); +}); + +router.post("/onboard", async (req: Request<{}, {}, UserDto>, res) => { + let userDto = req.body; + if (userDto === undefined) { + return res.status(400).send("Body is missing"); + } + if (userDto.organisation === undefined) { + return res.status(400).send("organisation is missing"); + } + + + const userDoc = await userService.onBoardUser(userDto); + res.json(userDoc); +}); + + +export default router; diff --git a/backend/src/routes/weather-route.ts b/backend/src/routes/weather-route.ts index 29dacd0b4..ca281f797 100644 --- a/backend/src/routes/weather-route.ts +++ b/backend/src/routes/weather-route.ts @@ -1,24 +1,31 @@ // import dependencies and initialize the express router -import { Router } from "express"; -import { GeoCode } from "integrations/weather-company-api.types"; -import { WeatherCompanyAPI } from "./../integrations/weather-company-api.service"; +import { Request, Router } from "express"; +import { isUndefined, toGroCodeFromPoint, UserDto } from "../../../common-types/src"; +import { weatherCompanyService } from "../integrations/weatherCompany/WeatherCompanyService"; +import { organisationService } from "../services/OrganisationService"; const router = Router(); -const api = new WeatherCompanyAPI(); - -const testMchinjiMalawiCoords: GeoCode = { - latitude: -13.7971726, - longitude: 32.8874963 -} +// const testMchinjiMalawiCoords: GeoCode = { +// latitude: -13.7971726, +// longitude: 32.8874963 +// } /** * Gets the forecast for a farmer. Right now this is hardcoded while we wait for farmer data from the session */ -router.get("/farmerForecast", async (req, res) => { +router.post("/farmerForecast", async (req: Request<{}, {}, UserDto>, res) => { console.log("farmerForecast"); - const geocode = testMchinjiMalawiCoords; - const forecast = await api.daily15DayForecast(geocode); + + const userDto = req.body; + + + const weatherCompanyConfig = await organisationService.getWeatherCompanyConfig(userDto.organisation.name); + if (isUndefined(weatherCompanyConfig)) { + return res.status(400).send( "User's organisation is not configured to use weather company"); + } + + const forecast = await weatherCompanyService.daily15DayForecast(weatherCompanyConfig, toGroCodeFromPoint(userDto.location)); // console.log(forecast); res.json(forecast); }); diff --git a/backend/src/services/crop.service.ts b/backend/src/services/CropService.ts similarity index 70% rename from backend/src/services/crop.service.ts rename to backend/src/services/CropService.ts index d2e49a25f..8cb92759a 100644 --- a/backend/src/services/crop.service.ts +++ b/backend/src/services/CropService.ts @@ -2,15 +2,10 @@ // const {cropDetailsView} = require("../db/cloudant"); // const {cropDetailsDdoc} = require("../db/cloudant"); -import { CropModel, Crop } from "../db/entities/crop"; +import { Crop } from "../../../common-types/src"; +import { CropModel } from "../db/entities/crop"; -// const APPLICATION_DB = "application-db"; -// const db = APPLICATION_DB; - -// const LOT_DB = "lot-areas"; -let cropDetails; - -export default class CropService { +export class CropService { constructor() { } @@ -36,4 +31,4 @@ export default class CropService { } } -// module.exports = CropService; +export const cropService = new CropService(); diff --git a/backend/src/services/FarmService.ts b/backend/src/services/FarmService.ts new file mode 100644 index 000000000..24fcbd1f5 --- /dev/null +++ b/backend/src/services/FarmService.ts @@ -0,0 +1,59 @@ +import { EISConfig, Farm, Farmer, isDefined, NewFarm } from "../../../common-types/src"; +import { FarmModel } from "../db/entities/farm"; +import { eisFarmService } from "../integrations/EIS/EISFarmService"; +import { organisationService } from "./OrganisationService"; + +export interface IFarmService { + getFarmerFarms(farmer: Farmer): Promise; + saveFarm(farmer: Farmer, newFarm: NewFarm): Promise; + saveFarms(farmer: Farmer, newFarms: NewFarm[]): Promise; +} + +class FarmService implements IFarmService { + + async getFarmerFarms(farmer: Farmer): Promise { + const eisConfig = await FarmService.getEISConfig(farmer.organisation); + + if (isDefined(eisConfig)) { + return await eisFarmService.getFarmerFarms(eisConfig, farmer); + } + + return await FarmModel.find({"farmer._id": farmer._id}).exec() as Farm[]; + } + + async saveFarm(farmer: Farmer, newFarm: NewFarm): Promise { + + const eisConfig = await FarmService.getEISConfig(farmer.organisation); + + if (isDefined(eisConfig)) { + return await eisFarmService.saveFarm(eisConfig, farmer, newFarm); + } + + const farmDoc = new FarmModel(newFarm); + return await farmDoc.save() as Farm; + } + + async saveFarms(farmer: Farmer, newFarms: NewFarm[]): Promise { + const eisConfig = await FarmService.getEISConfig(farmer.organisation); + const farms: Farm[] = []; + for (const newFarm of newFarms) { + let farm: Farm; + if (isDefined(eisConfig)) { + farm = await eisFarmService.saveFarm(eisConfig, farmer, newFarm); + } else { + farm = await this.saveFarm(farmer, newFarm) as Farm; + } + farms.push(farm); + } + + return farms; + } + + private static async getEISConfig(org: string): Promise { + return organisationService.getEISConfig(org); + } + +} + +export const farmService = new FarmService(); + diff --git a/backend/src/services/FarmerService.ts b/backend/src/services/FarmerService.ts new file mode 100644 index 000000000..04db6d757 --- /dev/null +++ b/backend/src/services/FarmerService.ts @@ -0,0 +1,32 @@ +import { FarmerModel } from "../db/entities/farmer"; +import { Farmer, NewFarmer } from "../../../common-types/src"; +import { farmService } from "./FarmService"; + +class FarmerService { + async getFarmers(): Promise { + return await FarmerModel.find().lean().exec(); + } + + async saveFarmer(newFarmer: NewFarmer): Promise { + const farmerDoc = new FarmerModel(newFarmer); + return await farmerDoc.save(); + } + + async getFarmer(id: string): Promise { + const farmer = await FarmerModel.findById(id).lean().exec(); + if (farmer == null) { + return null; + } + + // Get Fields + const farms = await farmService.getFarmerFarms(farmer); + + return { + ...farmer, + farms + }; + } +} + + +export const farmerService = new FarmerService() diff --git a/backend/src/services/OrganisationService.ts b/backend/src/services/OrganisationService.ts new file mode 100644 index 000000000..029fc4c9e --- /dev/null +++ b/backend/src/services/OrganisationService.ts @@ -0,0 +1,108 @@ +import { EISConfig, isDefined, isUndefined, Organisation, OrganisationDto, User, UserDto, UserOrganisationDto, WeatherCompanyConfig } from "../../../common-types/src"; +import { OrganisationModel } from "../db/entities/organisation"; +import { toUserDto } from "./UserService"; + + +class OrganisationService { + + async getOrganisationsByUserId(userId: string): Promise { + + const organisations = await OrganisationService.findAll({ + "users._id": userId + }); + + return organisations.map(org => toUserOrganisationDto(org)); + }; + + async getAllOrganisations(filter = {}): Promise { + const organisations = await OrganisationService.findAll(filter); + return organisations.map(org => toOrganisationDto(org)); + } + + async getOrganisation(id: string): Promise { + const org = await OrganisationModel.findById(id); + return isUndefined(org) ? null : toOrganisationDto(org); + } + + async createOrganisation(org: UserOrganisationDto): Promise { + const organisation = new OrganisationModel(); + + organisation.authMethod = org.authMethod; + organisation.users = org.users.map(userDto => { + const user: User = { + location: userDto.location, + mobile: userDto.mobile + } + return user; + }); + return toOrganisationDto(await organisation.save()); + } + + async addUserToOrganisation(userDto: UserDto): Promise { + const org = await OrganisationModel.findById(userDto.organisation) + if (isUndefined(org)) { + throw new Error("Organisation does not exist: " + userDto.organisation); + } + + let orgUser = org.users.find(orgUser => orgUser._id === user._id); + if (isDefined(orgUser)) { + return toUserDto(orgUser, org); + } + + const user: User = { + location: userDto.location, + _id: `${org.authMethod}:${userDto.id}`, + mobile: userDto.mobile + } + + org.users.push(user); + await org.save(); + return toUserDto(user, org); + } + + async getEISConfig(orgName: string): Promise { + const org = await OrganisationModel.findById(orgName); + + if (isUndefined(org)) { + throw new Error("Organisation does not exist: " + orgName); + } + + return org.eisConfig; + } + + async getWeatherCompanyConfig(orgName: string): Promise { + const org = await OrganisationModel.findById(orgName); + + if (isUndefined(org)) { + throw new Error("Organisation does not exist: " + orgName); + } + + return org.weatherCompanyConfig; + } + + + private static async findAll(filter = {}): Promise { + return OrganisationModel.find(filter).lean(); + } +} + +export const organisationService = new OrganisationService(); + +export function toOrganisationDto(org: Organisation): OrganisationDto { + return { + name: org.name, + authMethod: org.authMethod, + integrations: { + EIS: isDefined(org.eisConfig), + WEATHER: isDefined(org.weatherCompanyConfig) + } + }; +} + +export function toUserOrganisationDto(org: Organisation): UserOrganisationDto { + const dto = toOrganisationDto(org); + return { + ...dto, + users: org.users.map(user => toUserDto(user, org)) + }; +} diff --git a/backend/src/services/UserService.ts b/backend/src/services/UserService.ts new file mode 100644 index 000000000..6f1eb200f --- /dev/null +++ b/backend/src/services/UserService.ts @@ -0,0 +1,54 @@ +import { isUndefined, NewUserDto, Organisation, User, UserDto, UserOrganisationDto } from "../../../common-types/src"; +import { organisationService, toOrganisationDto } from "./OrganisationService"; + + +class UserService { + + async getUser(userId: string): Promise { + const orgs: UserOrganisationDto[] = await organisationService.getOrganisationsByUserId(userId); + + if (isUndefined(orgs) || orgs.length === 0) { + return []; + } + + const userDtos: UserDto[] = []; + + orgs.forEach(org => { + org.users.filter(user => user.id === userId).map(user => { + userDtos.push(toUserDto(user, org)); + }) + }); + + return userDtos; + } + + /** + * Explicit use of the word user here because it's the OAuth User we're talking about. + * provider + id + * e.g. "IBMid:1SD54A1" + */ + async doesUserExist(userDto: UserDto): Promise { + return (await this.getUser(userDto.id)) != null + } + + async onBoardUser(userDto: NewUserDto): Promise { + // Check if the organisation exists + return await organisationService.addUserToOrganisation(userDto); + + } +} + +export const userService = new UserService(); + +export function toUserDto(user: User, org: Organisation): UserDto { + if (isUndefined(user._id)) { + throw new Error("User does not have id.") + } + return { + email: "", + location: user.location, + mobile: user.mobile, + organisation: toOrganisationDto(org), + id: user._id + }; +} diff --git a/backend/src/services/coopManager.service.ts b/backend/src/services/coopManager.service.ts deleted file mode 100644 index befbd436b..000000000 --- a/backend/src/services/coopManager.service.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { CoopManagerModel, CoopManager } from "./../db/entities/coopManager"; -import { OrganisationModel, Organisation } from "./../db/entities/organisation"; - -export function getCoopManager(id: string) { - return CoopManagerModel.findById(id); -} - -/** - * Explicit use of the word user here because it's the OAuth User we're talking about. - * provider + id - * e.g. "IBMid:1SD54A1" - */ -export async function doesUserExist(id: string) { - const manager = await CoopManagerModel.findById(id); - return manager !== null; -} - -export async function onBoardUser(oAuthSource: string, oAuthId: string, user: CoopManager) { - // Check if the organisation exists - const orgs = await OrganisationModel.find({ - _id: { - $in: [user.coopOrganisations] - } - }); - if (orgs.length !== user.coopOrganisations.length) { - throw new Error("Organisation's given weren't found!"); - } - const newID = `${oAuthSource}:${oAuthId}` - user._id = newID; - const userDoc = await CoopManagerModel.create(user); - return userDoc; -} - -export async function addCoopManagerToOrganisation(coopManagerId: string, orgId: string) { - // Check if the Coop Manager exists - const coopManager = await CoopManagerModel.findById(coopManagerId); - if (coopManager == null) { - throw new Error("Coop Manager doesn't exist!"); - } - const org = await OrganisationModel.findById(orgId); - if (org == null) { - throw new Error("Organisation doesn't exist!"); - } - - if (coopManager.coopOrganisations.includes(coopManagerId)) { - return coopManager; - } - else { - coopManager.coopOrganisations.push(orgId); - const newCoopManager = await coopManager.save(); - return newCoopManager; - } -} - -// module.exports = CropService; diff --git a/backend/src/services/land-areas.service.ts b/backend/src/services/land-areas.service.ts index 17533015c..50ea97e0d 100644 --- a/backend/src/services/land-areas.service.ts +++ b/backend/src/services/land-areas.service.ts @@ -1,6 +1,4 @@ -import { Land, LandModel } from "../db/entities/land"; -import { Types } from 'mongoose'; -import { FarmerModel } from "./../db/entities/farmer"; +// import { Land, LandModel } from "../db/entities/land"; // const nswBbox = "140.965576,-37.614231,154.687500,-28.071980"; // lng lat // const nswBboxLatLng = "-37.614231,140.965576,-28.071980,154.687500"; // lat lng @@ -17,24 +15,24 @@ export interface BoundingBox { export default class LandAreasService { constructor() {} - async updateLot(lot: Land) { - const landModel = new LandModel(lot); - - const savedDoc = await landModel.save(); - return savedDoc; - } - - getLot(id: string) { - return LandModel.findById(id); - } - - getLots(ids: string[]) { - return LandModel.find({ '_id': { $in: ids } }); - } - - getAllLots() { - return LandModel.find(); - } + // async updateLot(lot: Land) { + // const landModel = new LandModel(lot); + // + // const savedDoc = await landModel.save(); + // return savedDoc; + // } + + // getLot(id: string) { + // return LandModel.findById(id); + // } + // + // getLots(ids: string[]) { + // return LandModel.find({ '_id': { $in: ids } }); + // } + // + // getAllLots() { + // return LandModel.find(); + // } getAreasInBbox(box: BoundingBox) { // const bbox = `${box.lowerLeft.lng},${box.lowerLeft.lat},${box.upperRight.lng},${box.upperRight.lat}`; @@ -162,14 +160,10 @@ export default class LandAreasService { return 0; } - getTotalFarmers() { - //return this.getViewValue(farmerCountDoc, farmerCountView, APPLICATION_DB); - return FarmerModel.count().exec(); - } getCropsPlanted() { // return this.getViewValue(cropsPlantedDoc, cropsPlantedView, LOT_DB); - + // Aggregate of crops planted return 0; } @@ -181,9 +175,9 @@ export default class LandAreasService { return 0; } - getTotalLots() { - return LandModel.count().exec(); - } + // getTotalLots() { + // return LandModel.count().exec(); + // } } // module.exports = LotAreas; diff --git a/backend/src/services/organisation.service.ts b/backend/src/services/organisation.service.ts deleted file mode 100644 index ab9dd6d22..000000000 --- a/backend/src/services/organisation.service.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { OrganisationModel, Organisation } from "./../db/entities/organisation"; -import { CoopManagerModel, CoopManager } from "./../db/entities/coopManager"; - -export function getAllOrganisations(lean = true) { - if (lean) - return OrganisationModel.find({}).lean(); - else - return OrganisationModel.find({}); -} - -export function getOrganisation(id: string) { - return OrganisationModel.findById(id); -} - -export function getOrganisations(id: string[], lean = true) { - const query = OrganisationModel.find({_id: {$in: id}}); - return lean ? query.lean() : query; -} - -export async function createOrganisationFromName(name: string) { - const orgModel = new OrganisationModel(); - orgModel.name = name; - // console.log(orgModel); - const orgDoc = orgModel.save(); - return orgDoc; -} - -export function createOrganisation(org: Organisation) { - return OrganisationModel.create(org); -} - -// module.exports = CropService; diff --git a/backend/src/services/recommendations.service.ts b/backend/src/services/recommendations.service.ts index 379aa4069..02d2a69d8 100644 --- a/backend/src/services/recommendations.service.ts +++ b/backend/src/services/recommendations.service.ts @@ -3,18 +3,17 @@ // const client = CloudantV1.newInstance({}); // const { plantedCrops, cropProductionForecast} = require("../db/cloudant"); -import LandAreasService from "./land-areas.service"; -import CropService from "./crop.service"; +import { cropService } from "./CropService"; // const nswBbox = "140.965576,-37.614231,154.687500,-28.071980"; // lng lat // const nswBboxLatLng = "-37.614231,140.965576,-28.071980,154.687500"; // lat lng -const weights = { - lowPlantedArea: 0.15, - lowYieldForecast: 0.20, - onShortlist: 0.25, - inSeason: 0.40, - // notOnOthersShortlist: 0.30, -}; +// const weights = { +// lowPlantedArea: 0.15, +// lowYieldForecast: 0.20, +// onShortlist: 0.25, +// inSeason: 0.40, +// // notOnOthersShortlist: 0.30, +// }; export interface RecommendationsRequest { plantDate: string @@ -22,17 +21,9 @@ export interface RecommendationsRequest { } export default class RecommendationsService { - lotAreaService: LandAreasService; - cropService: CropService; - - constructor() { - this.lotAreaService = new LandAreasService(); - this.cropService = new CropService(); - } - async getRecommendations(request: RecommendationsRequest) { this.createOrUpdateShortlistForLot(request); - const cropDetails = await this.cropService.getAllCrops(); + const cropDetails = await cropService.getAllCrops(); const plantDate = new Date(request.plantDate); const plantMonth = plantDate.getMonth() + 1; @@ -47,7 +38,7 @@ export default class RecommendationsService { const seasonStartMonth = startDate.getMonth() + 1; // let seasonEndMonth = crop.planting_season[1]; let seasonEndMonth = endDate.getMonth() + 1; - let inSeason = false; + let inSeason; if (seasonStartMonth > seasonEndMonth) { inSeason = (plantMonth >= seasonStartMonth && plantMonth <= 12) || (plantMonth >= 1 && plantMonth <= seasonEndMonth); } else { @@ -68,42 +59,42 @@ export default class RecommendationsService { crops[crop.toLowerCase()].shortlist = 100; }); - const overallCropDistribution: any = await this.lotAreaService.getOverallCropDistribution(); - const minArea = Math.min(...overallCropDistribution.map(dist => dist.area)); - - overallCropDistribution.forEach((dist) => { - if (crops[dist.crop.toLowerCase()]) { - crops[dist.crop.toLowerCase()].area = dist.area; - } - }); - - const cropProductionForecast: any = await this.lotAreaService.getCropProductionForecast(); - cropProductionForecast.forEach((dist) => { - const harvestDate = new Date(dist.date); - const crop = crops[dist.crop.toLowerCase()]; - if (harvestDate <= crop.harvestEnd && harvestDate >= crop.harvestStart) { - crop.yield += dist.yield; - } - }); - const minYield = Math.min(...cropDetails.map(crop => crops[crop.name.toLowerCase()].yield)); + // const overallCropDistribution: any = await this.lotAreaService.getOverallCropDistribution(); + // const minArea = Math.min(...overallCropDistribution.map(dist => dist.area)); + // + // overallCropDistribution.forEach((dist) => { + // if (crops[dist.crop.toLowerCase()]) { + // crops[dist.crop.toLowerCase()].area = dist.area; + // } + // }); + // + // const cropProductionForecast: any = await this.lotAreaService.getCropProductionForecast(); + // cropProductionForecast.forEach((dist) => { + // const harvestDate = new Date(dist.date); + // const crop = crops[dist.crop.toLowerCase()]; + // if (harvestDate <= crop.harvestEnd && harvestDate >= crop.harvestStart) { + // crop.yield += dist.yield; + // } + // }); + // const minYield = Math.min(...cropDetails.map(crop => crops[crop.name.toLowerCase()].yield)); const cropScores: any = []; - cropDetails.forEach((cropDetail) => { - const crop = crops[cropDetail.name.toLowerCase()]; - const cropScore: any = {}; - cropScore.crop = cropDetail.name; - cropScore.shortlistScore = crop.shortlist / 100 * weights.onShortlist; - cropScore.inSeasonScore = crop.inSeason / 100 * weights.inSeason; - cropScore.plantedAreaScore = crop.area === 0 ? 0 : (minArea / crop.area * weights.lowPlantedArea); - cropScore.yieldForecastScore = crop.yield === 0 ? 0 : (minYield / crop.yield * weights.lowYieldForecast); - cropScore.score = 10 * (cropScore.shortlistScore + - cropScore.inSeasonScore + - cropScore.plantedAreaScore + - cropScore.yieldForecastScore); - cropScores.push(cropScore); - }); - + // cropDetails.forEach((cropDetail) => { + // const crop = crops[cropDetail.name.toLowerCase()]; + // const cropScore: any = {}; + // cropScore.crop = cropDetail.name; + // cropScore.shortlistScore = crop.shortlist / 100 * weights.onShortlist; + // cropScore.inSeasonScore = crop.inSeason / 100 * weights.inSeason; + // cropScore.plantedAreaScore = crop.area === 0 ? 0 : (minArea / crop.area * weights.lowPlantedArea); + // cropScore.yieldForecastScore = crop.yield === 0 ? 0 : (minYield / crop.yield * weights.lowYieldForecast); + // cropScore.score = 10 * (cropScore.shortlistScore + + // cropScore.inSeasonScore + + // cropScore.plantedAreaScore + + // cropScore.yieldForecastScore); + // cropScores.push(cropScore); + // }); + // @ts-ignore return cropScores.sort((a, b) => b.score - a.score); } diff --git a/backend/src/sockets/socket.io.ts b/backend/src/sockets/socket.io.ts index c221c15d2..b07ab3d87 100644 --- a/backend/src/sockets/socket.io.ts +++ b/backend/src/sockets/socket.io.ts @@ -1,7 +1,7 @@ -import { MessageLog } from "./../db/entities/messageLog"; +import { MessageLog } from "../db/entities/messageLog"; import { Server as NodejsServer } from "http"; import { Namespace, Server } from "socket.io"; -import { Organisation } from "./../db/entities/organisation"; +import { Organisation } from "../../../common-types/src"; // interface ServerToClientEvents { @@ -67,7 +67,7 @@ export class SocketIOManager { getNamespaceOfOrg(org: Organisation) { this.ensureInitialised() - return this.ioServer.of(`/org-${org._id}`); + return this.ioServer.of(`/org-${org.name}`); } publishMessage(org: Organisation, message: MessageLog) { @@ -77,7 +77,7 @@ export class SocketIOManager { publish(org: Organisation, event: string, ...args: any) { this.ensureInitialised() - console.log("[Socket IO Server] Publishing Event. Namespace:", `/org-${org._id}`, "Event:", event, "Args", ...args); + console.log("[Socket IO Server] Publishing Event. Namespace:", `/org-${org.name}`, "Event:", event, "Args", ...args); const nsp = this.getNamespaceOfOrg(org); nsp.emit(event, ...args); diff --git a/backend/src/types.d.ts b/backend/src/types.d.ts index 5bbcea49c..12f024eea 100644 --- a/backend/src/types.d.ts +++ b/backend/src/types.d.ts @@ -1,5 +1,3 @@ -import * as express from "express" - declare global { namespace Express { interface Request { @@ -7,4 +5,4 @@ declare global { session: any } } -} \ No newline at end of file +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 9b16f8306..5721658b3 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -1,12 +1,13 @@ { + "extends": "../tsconfig.settings.json", "include": ["src/**/*"], "exclude": ["node_modules", "**/*.spec.ts"], - "ts-node": { - "files": true - }, "files": [ "src/types.d.ts" ], + "references": [ + { "path": "../common-types" } + ], "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ @@ -14,12 +15,12 @@ "incremental": true, /* Enable incremental compilation */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ "tsBuildInfoFile": "./tsbuildinfo", /* Specify the folder for .tsbuildinfo incremental compilation files. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ + "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ @@ -83,7 +84,7 @@ /* Type Checking */ "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ + "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ diff --git a/common-types/Fields.ts b/common-types/Fields.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/common-types/data-model/farmer.ts b/common-types/data-model/farmer.ts deleted file mode 100644 index d411c96f2..000000000 --- a/common-types/data-model/farmer.ts +++ /dev/null @@ -1,23 +0,0 @@ - - -export interface Farmer { - _id?: Types.ObjectId, - name: string, - mobile: string[], - coopOrganisations: string[] - land_ids: string[] - lands?: Land[] -} - -export const FarmerSchema = new Schema({ - _id: { - type: ObjectId, - auto: true - }, - name: String, - mobile: [String], - coopOrganisations: [String], - land_ids: [ObjectId] -}); - -export const FarmerModel = model("farmer", FarmerSchema); \ No newline at end of file diff --git a/common-types/data-model/land.ts b/common-types/data-model/land.ts deleted file mode 100644 index b2a1e2474..000000000 --- a/common-types/data-model/land.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Using Node.js `require()` -import { Schema, Model, model, Types } from 'mongoose'; -import { CropSchema, Crop } from "./crop"; -import { FarmerSchema, Farmer } from './farmer'; - -const ObjectId = Schema.Types.ObjectId; - -export interface FarmerCrop { - _id?: Types.ObjectId, - farmer: Farmer, - crop: Crop -} - -export const FarmerCropSchema = new Schema({ - _id: ObjectId, - farmer: FarmerSchema, - crop: CropSchema -}); - -export interface Land { - _id?: Types.ObjectId, - type: string, - fid: number, - name: string, - crops: FarmerCrop[] -} - -export const LandSchema = new Schema({ - _id: ObjectId, - type: String, - fid: Number, - name: String, - crops: [FarmerCropSchema] -}); - -export const LandModel = model("land", LandSchema); \ No newline at end of file diff --git a/common-types/data-model/organisation.ts b/common-types/data-model/organisation.ts deleted file mode 100644 index 0ec005eb2..000000000 --- a/common-types/data-model/organisation.ts +++ /dev/null @@ -1,20 +0,0 @@ - -import { Schema, model, ObjectId, Types } from 'mongoose'; -import { Land } from './land'; - -const ObjectId = Schema.Types.ObjectId; - -export interface Organisation { - _id?: Types.ObjectId, - name: string -} - -export const OrganisationSchema = new Schema({ - _id: { - type: ObjectId, - auto: true - }, - name: String, -}); - -export const OrganisationModel = model("organisation", OrganisationSchema); diff --git a/common-types/package.json b/common-types/package.json new file mode 100644 index 000000000..44b0cef63 --- /dev/null +++ b/common-types/package.json @@ -0,0 +1,233 @@ +{ + "name": "@openharvest/common-types", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "version": "1.0.0", + "dependencies": { + "@types/geojson": "^7946.0.8" + }, + "eslintConfig": { + "overrides": [ + { + "files": [ + "**/*.ts?(x)" + ], + "extends": [ + "plugin:react/recommended" + ], + "plugins": [ + "eslint-plugin-prefer-arrow", + "eslint-plugin-react", + "@typescript-eslint" + ], + "rules": { + "@typescript-eslint/adjacent-overload-signatures": "warn", + "@typescript-eslint/array-type": [ + "warn", + { + "default": "array" + } + ], + "@typescript-eslint/ban-types": [ + "warn", + { + "types": { + "Object": { + "message": "Avoid using the `Object` type. Did you mean `object`?" + }, + "Function": { + "message": "Avoid using the `Function` type. Prefer a specific function type, like `() => void`." + }, + "Boolean": { + "message": "Avoid using the `Boolean` type. Did you mean `boolean`?" + }, + "Number": { + "message": "Avoid using the `Number` type. Did you mean `number`?" + }, + "String": { + "message": "Avoid using the `String` type. Did you mean `string`?" + }, + "Symbol": { + "message": "Avoid using the `Symbol` type. Did you mean `symbol`?" + } + } + } + ], + "@typescript-eslint/consistent-type-assertions": "warn", + "@typescript-eslint/dot-notation": "off", + "@typescript-eslint/explicit-function-return-type": "warn", + "@typescript-eslint/explicit-member-accessibility": [ + "off", + { + "accessibility": "explicit" + } + ], + "@typescript-eslint/indent": "warn", + "@typescript-eslint/member-delimiter-style": [ + "warn", + { + "multiline": { + "delimiter": "semi", + "requireLast": true + }, + "singleline": { + "delimiter": "semi", + "requireLast": false + } + } + ], + "@typescript-eslint/member-ordering": "warn", + "@typescript-eslint/naming-convention": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-empty-interface": "warn", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-inferrable-types": [ + "warn", + { + "ignoreParameters": true + } + ], + "@typescript-eslint/no-misused-new": "warn", + "@typescript-eslint/no-namespace": "warn", + "@typescript-eslint/no-parameter-properties": "off", + "@typescript-eslint/no-shadow": [ + "warn", + { + "hoist": "all" + } + ], + "@typescript-eslint/no-unused-expressions": "warn", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/no-var-requires": "warn", + "@typescript-eslint/prefer-for-of": "warn", + "@typescript-eslint/prefer-function-type": "warn", + "@typescript-eslint/prefer-namespace-keyword": "warn", + "@typescript-eslint/quotes": [ + "warn", + "double" + ], + "@typescript-eslint/semi": [ + "warn", + "always" + ], + "@typescript-eslint/triple-slash-reference": [ + "warn", + { + "path": "always", + "types": "prefer-import", + "lib": "always" + } + ], + "@typescript-eslint/type-annotation-spacing": "warn", + "@typescript-eslint/unified-signatures": "warn", + "brace-style": [ + "warn", + "1tbs" + ], + "complexity": "off", + "constructor-super": "warn", + "curly": "warn", + "eol-last": "warn", + "eqeqeq": [ + "warn", + "smart" + ], + "guard-for-in": "warn", + "id-blacklist": [ + "warn", + "any", + "Number", + "number", + "String", + "string", + "Boolean", + "boolean", + "Undefined", + "undefined" + ], + "id-match": "warn", + "indent": "off", + "max-classes-per-file": [ + "warn", + 1 + ], + "max-len": [ + "warn", + { + "code": 200 + } + ], + "new-parens": "warn", + "no-bitwise": "warn", + "no-caller": "warn", + "no-cond-assign": "warn", + "no-console": [ + "warn", + { + "allow": [ + "log", + "warn", + "dir", + "timeLog", + "assert", + "clear", + "count", + "countReset", + "group", + "groupEnd", + "table", + "dirxml", + "error", + "groupCollapsed", + "Console", + "profile", + "profileEnd", + "timeStamp", + "context" + ] + } + ], + "no-debugger": "warn", + "no-empty": "off", + "no-eval": "warn", + "no-fallthrough": "warn", + "no-invalid-this": "off", + "no-new-wrappers": "warn", + "no-redeclare": "warn", + "no-restricted-imports": "warn", + "no-throw-literal": "warn", + "no-trailing-spaces": "warn", + "no-undef-init": "warn", + "no-underscore-dangle": "off", + "no-unsafe-finally": "warn", + "no-unused-labels": "warn", + "no-var": "warn", + "object-shorthand": "warn", + "one-var": [ + "warn", + "never" + ], + "prefer-arrow/prefer-arrow-functions": "warn", + "prefer-const": "warn", + "radix": "warn", + "react/jsx-boolean-value": "warn", + "react/jsx-key": "warn", + "react/jsx-no-bind": "warn", + "react/self-closing-comp": "warn", + "spaced-comment": [ + "warn", + "always", + { + "markers": [ + "/" + ] + } + ], + "use-isnan": "warn", + "valid-typeof": "off", + "react/display-name": "warn" + } + } + ] + } +} diff --git a/common-types/data-model/crop.ts b/common-types/src/data-model/Crop.ts similarity index 54% rename from common-types/data-model/crop.ts rename to common-types/src/data-model/Crop.ts index f83f9d559..9bb3894b8 100644 --- a/common-types/data-model/crop.ts +++ b/common-types/src/data-model/Crop.ts @@ -1,8 +1,9 @@ -export interface Crop { +export default interface Crop { _id?: string, type: string, name: string, - planting_season: Date[], + planting_season: number[], time_to_harvest: number, + yield_per_sqm: number, is_ongoing: boolean } diff --git a/common-types/src/data-model/Farm.ts b/common-types/src/data-model/Farm.ts new file mode 100644 index 000000000..97114fef7 --- /dev/null +++ b/common-types/src/data-model/Farm.ts @@ -0,0 +1,16 @@ +import { Polygon } from "geojson"; +import Field, { NewField } from "./Field"; + +export interface NewFarm { + _id?: string, + name: string; + fields: NewField[]; + geometry?: Polygon; +} + +export default interface Farm extends NewFarm { + _id: string, + fields: Field[] +} + + diff --git a/common-types/src/data-model/Farmer.ts b/common-types/src/data-model/Farmer.ts new file mode 100644 index 000000000..f415406d9 --- /dev/null +++ b/common-types/src/data-model/Farmer.ts @@ -0,0 +1,15 @@ +import Farm, { NewFarm } from "./Farm"; + +export interface NewFarmer { + _id?: string, + name: string, + mobile: string[], + address: string, + organisation: string, + farms: NewFarm[] +} + +export default interface Farmer extends NewFarmer{ + _id: string, + farms: Farm[] +} diff --git a/common-types/src/data-model/Field.ts b/common-types/src/data-model/Field.ts new file mode 100644 index 000000000..6b18ea8ad --- /dev/null +++ b/common-types/src/data-model/Field.ts @@ -0,0 +1,14 @@ +import { Polygon } from "geojson"; +import FieldCrop from "./FieldCrop"; + + +export interface NewField { + _id?: string, + name: string; + geometry: Polygon; + crops: FieldCrop[] +} + +export default interface Field extends NewField { + _id: string, +} diff --git a/common-types/src/data-model/FieldCrop.ts b/common-types/src/data-model/FieldCrop.ts new file mode 100644 index 000000000..ebd17ad39 --- /dev/null +++ b/common-types/src/data-model/FieldCrop.ts @@ -0,0 +1,7 @@ +import Crop from "./Crop"; + +export default interface FieldCrop { + crop: Crop, + planted_date: Date, + harvested_date?: Date +} diff --git a/common-types/src/data-model/Organisation.ts b/common-types/src/data-model/Organisation.ts new file mode 100644 index 000000000..5ca5994c2 --- /dev/null +++ b/common-types/src/data-model/Organisation.ts @@ -0,0 +1,11 @@ +import User from "./User"; +import { AuthMethod } from "../globals"; +import { EISConfig, WeatherCompanyConfig } from "../index"; + +export default interface Organisation { + name: string, + authMethod: AuthMethod, + users: User[], + eisConfig?: EISConfig, + weatherCompanyConfig?: WeatherCompanyConfig +} diff --git a/common-types/data-model/coopManager.ts b/common-types/src/data-model/User.ts similarity index 59% rename from common-types/data-model/coopManager.ts rename to common-types/src/data-model/User.ts index 5824cfca4..30d2d3a06 100644 --- a/common-types/data-model/coopManager.ts +++ b/common-types/src/data-model/User.ts @@ -1,5 +1,6 @@ +import { Point } from "geojson"; -export interface CoopManager { +export default interface User { /** * Auth provider + auth provider id. E.g. "IBMid:1SDAS61W6A" */ @@ -7,7 +8,8 @@ export interface CoopManager { /** * GeoCode / LatLng coordinate tuple */ - location: number[], - mobile: string, - coopOrganisations: string[] + location: Point, + mobile: string } + + diff --git a/common-types/src/dto/OrganisationDto.ts b/common-types/src/dto/OrganisationDto.ts new file mode 100644 index 000000000..0d60b56b8 --- /dev/null +++ b/common-types/src/dto/OrganisationDto.ts @@ -0,0 +1,12 @@ +import { UserDto } from "./UserDto"; +import { AuthMethod, Integration } from "../globals"; + +export default interface OrganisationDto { + name: string; + authMethod: AuthMethod; + integrations: Partial> +} + +export interface UserOrganisationDto extends OrganisationDto{ + users: Omit[]; +} diff --git a/common-types/src/dto/UserDto.ts b/common-types/src/dto/UserDto.ts new file mode 100644 index 000000000..37853b3b2 --- /dev/null +++ b/common-types/src/dto/UserDto.ts @@ -0,0 +1,15 @@ +import { Point } from "geojson"; +import OrganisationDto from "./OrganisationDto"; + +export type UserDto = { + location: Point; + id: string; + email: string; + organisation: OrganisationDto, + mobile: string +} + +export type NewUserDto = UserDto & { + id?: string +} + diff --git a/backend/src/integrations/weather-company-api.types.ts b/common-types/src/globals.ts similarity index 86% rename from backend/src/integrations/weather-company-api.types.ts rename to common-types/src/globals.ts index 61aea2c65..67406a5ce 100644 --- a/backend/src/integrations/weather-company-api.types.ts +++ b/common-types/src/globals.ts @@ -1,4 +1,4 @@ -export enum Languages { +export enum Language { Amharic_Ethiopia = "am-ET", Arabic_United_Arab_Emirates = "ar-AE", Azerbaijani_Azerbaijan = "az-AZ", @@ -92,7 +92,7 @@ export enum Languages { /** * Measurement units to return from the API */ -export enum Units { +export enum Unit { imperial = "e", metric = "m", /** @@ -101,28 +101,29 @@ export enum Units { SI = "s" } -/** - * LatLng representation for the Weather Company - */ -export interface GeoCode { - latitude: number; - longitude: number; -} - /** * Result format of the API Call * NOTE: Not every api call supports CSV. */ -export enum Formats { +export enum DataFormat { JSON = "json", CSV = "csv" } -export interface CommonOptions { - format: Formats; - language: Languages; - units: Units -} +export const isDefined = (value: T | null | undefined): value is T => { + return value !== undefined && value != null; +}; + +export const isUndefined = (value: T | null | undefined): value is undefined | null => { + return value === undefined || value == null; +}; -export type LatLng = GeoCode; +export enum AuthMethod { + IBM_ID = "IBM_ID" +} +export enum Integration { + EIS = "EIS", + WEATHER = "WEATHER", + MESSAGING = "MESSAGING", +} diff --git a/common-types/src/index.ts b/common-types/src/index.ts new file mode 100644 index 000000000..8817b7f57 --- /dev/null +++ b/common-types/src/index.ts @@ -0,0 +1,71 @@ +/** + * LatLng representation for the Weather Company + */ +import { DataFormat, Integration, isDefined, isUndefined, Language, Unit } from "./globals"; +import Crop from "./data-model/Crop"; +import Farm, { NewFarm } from "./data-model/Farm"; +import User from "./data-model/User"; +import Farmer, { NewFarmer } from "./data-model/Farmer"; +import Organisation from "./data-model/Organisation"; +import FieldCrop from "./data-model/FieldCrop"; +import Field, { NewField } from "./data-model/Field"; +import { NewUserDto, UserDto } from "./dto/UserDto"; +import OrganisationDto, { UserOrganisationDto } from "./dto/OrganisationDto"; +import { EISConfig } from "./integrations/EISConfig"; +import { WeatherCompanyConfig } from "./integrations/WeatherCompanyConfig"; +import { Point } from "geojson"; + +export { DataFormat, Language, Unit, isDefined, isUndefined }; +export { Crop, Farm, NewFarm, User, Farmer, NewFarmer, Organisation, Field, NewField, FieldCrop, Integration}; + +export {EISConfig, WeatherCompanyConfig} + +export {UserDto, NewUserDto, OrganisationDto, UserOrganisationDto}; + +export type LatLngNumber = { + lat: number, + lng: number +} + +export type LatLngString = { + lat: string, + lng: string +} + +export type GeoCodeNumber = { + latitude: number, + longitude: number +} + +export function toLatLng(geoCode: GeoCodeNumber): LatLngNumber { + return { + lat: geoCode.latitude, + lng: geoCode.longitude + } +} + +export function toGeoCode(latLng: LatLngNumber): GeoCodeNumber { + return { + latitude: latLng.lat, + longitude: latLng.lng + } +} + +export function toGroCodeFromPoint(point: Point): GeoCodeNumber { + return { + latitude: point.coordinates[0], + longitude: point.coordinates[1] + } +} + +export function geoCodeToString(geocode: GeoCodeNumber) { + return `${geocode.latitude},${geocode.longitude}` +} + +export interface CommonOptions { + format: DataFormat; + language: Language; + units: Unit +} + + diff --git a/common-types/src/integrations/EISConfig.ts b/common-types/src/integrations/EISConfig.ts new file mode 100644 index 000000000..f236e132f --- /dev/null +++ b/common-types/src/integrations/EISConfig.ts @@ -0,0 +1,6 @@ +export type EISConfig = { + apiUrl: string; + tokenUrl: string; + apiKey: string; + clientId: string; +} diff --git a/common-types/src/integrations/WeatherCompanyConfig.ts b/common-types/src/integrations/WeatherCompanyConfig.ts new file mode 100644 index 000000000..2ac219104 --- /dev/null +++ b/common-types/src/integrations/WeatherCompanyConfig.ts @@ -0,0 +1,7 @@ +import { CommonOptions } from "../index"; + +export type WeatherCompanyConfig = { + apiUrl: string; + apiKey: string; + options?: CommonOptions +} diff --git a/common-types/tsconfig.json b/common-types/tsconfig.json new file mode 100644 index 000000000..b255b60a7 --- /dev/null +++ b/common-types/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "composite": true, + "target": "es6", + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "module": "commonjs", + "isolatedModules": false, + "jsx": "preserve", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "noImplicitAny": true, + "typeRoots": [ + "src/index.ts" + ] + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 37be3e9d8..56a6000c3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,3 +8,15 @@ services: - "./backend/.env.dev.production:/home/node/app/.env:ro" ports: - "3000:3000" + + mongodb: + container_name: 'mongodb-open_harvest' + image: mongo + restart: unless-stopped + environment: + - PUID=1000 + - PGID=1000 + ports: + - 27017:27017 + volumes: + - ~/mongodb:/data/db diff --git a/package.json b/package.json deleted file mode 100644 index 12a94bf0f..000000000 --- a/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "dependencies": { - "@carbon/charts": "^0.54.12", - "@carbon/charts-react": "^0.54.12", - "carbon-components": "^10.54.0", - "d3": "^7.3.0" - } -} diff --git a/react-app/package.json b/react-app/package.json index 0cabab6ab..da84cf742 100644 --- a/react-app/package.json +++ b/react-app/package.json @@ -1,5 +1,5 @@ { - "name": "react-app", + "name": "@openharvest/frontend", "version": "0.1.0", "private": true, "dependencies": { diff --git a/react-app/src/App.tsx b/react-app/src/App.tsx index 4a7492b9d..37372d477 100644 --- a/react-app/src/App.tsx +++ b/react-app/src/App.tsx @@ -3,40 +3,36 @@ import "carbon-addons-iot-react/scss/styles.scss"; import PrivateRoute from "./helpers/privateRoute"; import "./App.scss"; -import { Content, Header, HeaderContainer, HeaderMenuItem, HeaderName, HeaderNavigation, SkipToContent } from "carbon-components-react"; +import { Content, HeaderContainer } from "carbon-components-react"; import { Redirect, Route, Switch, withRouter } from "react-router"; import { RouteComponentProps } from "react-router/ts4.0"; -import Nav from "./components/Nav/Nav" +import Nav from "./components/Nav/Nav"; import CoOpHome from "./components/CoOpHome/CoOpHome"; import Farmers from "./components/Farmers/Farmers"; import Crops from "./components/Crops/Crops"; import FoodTrust from "./components/FoodTrust/FoodTrust"; -import { AuthContext, AuthProvider } from "./services/auth"; -import UserOnboarding from "./components/Onboarding/UserOnboarding"; +import { AuthProvider } from "./services/auth"; +import UserOnBoarding from "./components/Onboarding/UserOnboarding"; import { AddFarmer } from "./components/Farmers/AddFarmer"; -import {enableAllPlugins} from "immer" +import { enableAllPlugins } from "immer"; import { Messaging } from "./components/Messaging/Messaging"; + enableAllPlugins(); type AppProps = RouteComponentProps ; type AppState = { // showOnBoardingWizard: boolean; - showLogoutModal: boolean; + showLogoutModal?: boolean; + newUser?: boolean; }; class App extends Component { constructor(props: any) { super(props); - console.log(props.location); - this.state = { - // showOnBoardingWizard: false, - showLogoutModal: false - }; + this.state = {}; this.setShowLogoutModal = this.setShowLogoutModal.bind(this); - - } async componentDidMount() { @@ -46,15 +42,12 @@ class App extends Component { const res = await fetch("/api/coopManager/hasBeenOnBoarded"); const result = await res.json(); newUser = result.exists; - } - catch (e) {} - + } catch (e) {} + + this.setState({newUser}); + if (newUser) { - // this.state = { - // // showOnBoardingWizard: true, - // showLogoutModal: false - // }; - this.props.history.push('/onboarding') + this.props.history.push("/onboarding"); } } @@ -64,82 +57,88 @@ class App extends Component { <> ( + render={() => (