diff --git a/.vscode/launch.json b/.vscode/launch.json index 905810ecb3..986f35fae8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,8 +12,8 @@ }, { "address": "0.0.0.0", - "localRoot": "${workspaceFolder}/backend", - "name": "Backend: Debug with Docker", + "localRoot": "${workspaceFolder}/DashboardAPI", + "name": "Dashboard API: Debug with Docker", "port": 9232, "remoteRoot": "/usr/src/app", "request": "attach", diff --git a/Common/Models/Project.ts b/Common/Models/Project.ts index aa4861fe01..751c9fe55e 100644 --- a/Common/Models/Project.ts +++ b/Common/Models/Project.ts @@ -9,12 +9,28 @@ import TableColumn from '../Types/Database/TableColumn'; import CrudApiEndpoint from '../Types/Database/CrudApiEndpoint'; import Route from '../Types/API/Route'; import TableColumnType from '../Types/Database/TableColumnType'; +import UserRecordPermissions from '../Types/Database/AccessControls/User/UserRecordPermissions'; +import UserColumnPermissions from '../Types/Database/AccessControls/User/UserColumnPermissions'; @CrudApiEndpoint(new Route('/project')) +@UserRecordPermissions({ + create: true, + readAsList: false, + readAsItem: false, + update: false, + delete: false, +}) @Entity({ name: 'Project', }) export default class Model extends BaseModel { + @UserColumnPermissions({ + create: true, + readAsList: false, + readAsItem: false, + update: false, + delete: false, + }) @TableColumn({ required: true, type: TableColumnType.ShortText }) @Column({ nullable: false, diff --git a/Common/Types/Database/CrudApiEndpoint.ts b/Common/Types/Database/CrudApiEndpoint.ts index ae456809b8..d8d13cfca0 100644 --- a/Common/Types/Database/CrudApiEndpoint.ts +++ b/Common/Types/Database/CrudApiEndpoint.ts @@ -2,6 +2,6 @@ import Route from '../API/Route'; export default (apiPath: Route) => { return (ctr: Function) => { - ctr.prototype.CrudApiPath = apiPath; + ctr.prototype.crudApiPath = apiPath; }; }; diff --git a/Common/Types/Role.ts b/Common/Types/Role.ts index 6b6b4b0f2c..4f05cd7cc7 100644 --- a/Common/Types/Role.ts +++ b/Common/Types/Role.ts @@ -1,9 +1,10 @@ enum Role { - Owner = 'Owner', - Administrator = 'Administrator', - Member = 'Member', - Viewer = 'Viewer', - Public = 'Public', + Owner = 'Owner', // owner of a project. An owner owns all the billing info. + Administrator = 'Administrator', // admin of a project + Member = 'Member', // member of a project + Viewer = 'Viewer', // user who is a viewer in a project + User = 'User', // registered-user but does not belong to a project + Public = 'Public', // non-registered user. } export const RoleArray: Array = [...new Set(Object.keys(Role))]; // Returns ["Owner", "Administrator"...] diff --git a/CommonServer/API/BaseAPI.ts b/CommonServer/API/BaseAPI.ts index a407bcc027..495840a8c2 100644 --- a/CommonServer/API/BaseAPI.ts +++ b/CommonServer/API/BaseAPI.ts @@ -28,35 +28,35 @@ export default class BaseAPI< // Create router.post( - `${this.entityType.name}/`, + `/${new this.entityType().getCrudApiPath()?.toString()}`, UserMiddleware.getUserMiddleware, this.createItem ); // List router.get( - `${this.entityType.name}/list`, + `/${new this.entityType().getCrudApiPath()?.toString()}/list`, UserMiddleware.getUserMiddleware, this.getList ); // Get Item router.get( - `${this.entityType.name}/:id`, + `/${new this.entityType().getCrudApiPath()?.toString()}/:id`, UserMiddleware.getUserMiddleware, this.getItem ); // Update router.put( - `${this.entityType.name}/:id`, + `/${new this.entityType().getCrudApiPath()?.toString()}/:id`, UserMiddleware.getUserMiddleware, this.updateItem ); // Delete router.delete( - `${this.entityType.name}/:id`, + `/${new this.entityType().getCrudApiPath()?.toString()}/:id`, UserMiddleware.getUserMiddleware, this.deleteItem ); diff --git a/CommonServer/Utils/StartServer.ts b/CommonServer/Utils/StartServer.ts index 3d6249ed95..4222e3ee16 100644 --- a/CommonServer/Utils/StartServer.ts +++ b/CommonServer/Utils/StartServer.ts @@ -38,17 +38,13 @@ const logRequest: RequestHandler = ( const method: string = req.method; const url: string = req.url; - const header_info: string = `Request ID: ${ - (req as OneUptimeRequest).id - } -- POD NAME: ${ - process.env['POD_NAME'] || 'NONE' - } -- METHOD: ${method} -- URL: ${url.toString()}`; + const header_info: string = `Request ID: ${(req as OneUptimeRequest).id + } -- POD NAME: ${process.env['POD_NAME'] || 'NONE' + } -- METHOD: ${method} -- URL: ${url.toString()}`; - const body_info: string = `Request ID: ${ - (req as OneUptimeRequest).id - } -- Request Body: ${ - req.body ? JSON.stringify(req.body, null, 2) : 'EMPTY' - }`; + const body_info: string = `Request ID: ${(req as OneUptimeRequest).id + } -- Request Body: ${req.body ? JSON.stringify(req.body, null, 2) : 'EMPTY' + }`; logger.info(header_info + '\n ' + body_info); next(); @@ -115,6 +111,22 @@ const init: Function = async (appName: string): Promise => { } ); + app.post('*', function (_req, res) { + res.status(404).json({ "error": "API not found" }); + }); + + app.put('*', function (_req, res) { + res.status(404).json({ "error": "API not found" }); + }); + + app.delete('*', function (_req, res) { + res.status(404).json({ "error": "API not found" }); + }); + + app.get('*', function (_req, res) { + res.status(404).json({ "error": "API not found" }); + }); + return app; }; diff --git a/CommonUI/.env b/CommonUI/.env index 48fe67c5ec..e52f63937f 100644 --- a/CommonUI/.env +++ b/CommonUI/.env @@ -1,7 +1,7 @@ REALTIME_ROUTE=/realtime MAIL_ROUTE=/mail -DASHBOARD_ROUTE=dashboard -DASHBOARD_API_ROUTE=/dashboard-api +DASHBOARD_ROUTE=/dashboard +DASHBOARD_API_ROUTE=/api PROBE_API_ROUTE=/probe-api DATA_INGESTOR_ROUTE=/data-ingestor ACCOUNTS_ROUTE=/accounts diff --git a/CommonUI/src/Components/Forms/BasicForm.tsx b/CommonUI/src/Components/Forms/BasicForm.tsx index 592d7032c5..0475bdfd6d 100644 --- a/CommonUI/src/Components/Forms/BasicForm.tsx +++ b/CommonUI/src/Components/Forms/BasicForm.tsx @@ -1,5 +1,5 @@ -import React, { ReactElement } from 'react'; -import { ErrorMessage, Field, Form, Formik, FormikErrors } from 'formik'; +import React, { MutableRefObject, ReactElement, useRef } from 'react'; +import { ErrorMessage, Field, Form, Formik, FormikErrors, FormikProps, FormikValues } from 'formik'; import Button, { ButtonStyleType } from '../Button/Button'; import FormValues from './Types/FormValues'; import Fields from './Types/Fields'; @@ -35,6 +35,7 @@ export interface ComponentProps { maxPrimaryButtonWidth?: boolean; error: string | null; hideSubmitButton?: boolean; + formRef?: MutableRefObject> } function getFieldType(fieldType: FormFieldSchemaType): string { @@ -53,6 +54,7 @@ function getFieldType(fieldType: FormFieldSchemaType): string { const BasicForm: Function = ( props: ComponentProps ): ReactElement => { + const getFormField: Function = ( field: DataField, index: number, @@ -246,12 +248,15 @@ const BasicForm: Function = ( return { ...errors, ...customValidateResult } as FormikErrors< FormValues >; - }; + }; + + const formRef: any = useRef(null); return (
{ maxPrimaryButtonWidth?: boolean; error: string | null; hideSubmitButton?: boolean; + formRef?: MutableRefObject> } const BasicModelForm: Function = ( @@ -81,6 +82,7 @@ const BasicModelForm: Function = ( maxPrimaryButtonWidth={props.maxPrimaryButtonWidth || false} error={props.error} hideSubmitButton={props.hideSubmitButton} + formRef={props.formRef} > ); }; diff --git a/CommonUI/src/Components/Forms/ModelForm.tsx b/CommonUI/src/Components/Forms/ModelForm.tsx index 538fe57dbb..c394746eb9 100644 --- a/CommonUI/src/Components/Forms/ModelForm.tsx +++ b/CommonUI/src/Components/Forms/ModelForm.tsx @@ -1,5 +1,5 @@ -import React, { ReactElement, useState } from 'react'; -import { FormikErrors } from 'formik'; +import React, { MutableRefObject, ReactElement, useState } from 'react'; +import { FormikErrors, FormikProps, FormikValues } from 'formik'; import BaseModel from 'Common/Models/BaseModel'; import FormValues from './Types/FormValues'; import Fields from './Types/Fields'; @@ -39,6 +39,8 @@ export interface ComponentProps { apiUrl?: URL; formType: FormType; hideSubmitButton?: boolean; + formRef?: MutableRefObject>; + onLoadingChange?: (isLoading: boolean) => void; } const ModelForm: Function = ( @@ -51,6 +53,9 @@ const ModelForm: Function = ( // Ping an API here. setError(''); setLoading(true); + if (props.onLoadingChange) { + props.onLoadingChange(true); + } let apiUrl: URL | null = props.apiUrl || null; if (!apiUrl) { @@ -61,7 +66,7 @@ const ModelForm: Function = ( ); } - apiUrl = DASHBOARD_API_URL.addRoute(apiPath); + apiUrl = URL.fromURL(DASHBOARD_API_URL).addRoute(apiPath); } const result: HTTPResponse< @@ -77,6 +82,9 @@ const ModelForm: Function = ( ); setLoading(false); + if (props.onLoadingChange) { + props.onLoadingChange(false); + } if (result.isSuccess()) { if (props.onSuccess) { @@ -105,6 +113,7 @@ const ModelForm: Function = ( maxPrimaryButtonWidth={props.maxPrimaryButtonWidth} error={error} hideSubmitButton={props.hideSubmitButton} + formRef={props.formRef} > ); }; diff --git a/CommonUI/src/Components/Modal/Modal.tsx b/CommonUI/src/Components/Modal/Modal.tsx index 8d5eab82c1..66cde71a1e 100644 --- a/CommonUI/src/Components/Modal/Modal.tsx +++ b/CommonUI/src/Components/Modal/Modal.tsx @@ -13,6 +13,7 @@ export interface ComponentProps { onSubmit: () => void; submitButtonStyleType?: ButtonStyleType; submitButtonType?: ButtonType; + isLoading?: boolean; } const Modal: FunctionComponent = ( @@ -51,6 +52,7 @@ const Modal: FunctionComponent = ( onClose={ props.onClose ? props.onClose : undefined } + isLoading={props.isLoading || false} />
diff --git a/CommonUI/src/Components/Modal/ModalFooter.tsx b/CommonUI/src/Components/Modal/ModalFooter.tsx index 76bbeebfa5..0d063cefa2 100644 --- a/CommonUI/src/Components/Modal/ModalFooter.tsx +++ b/CommonUI/src/Components/Modal/ModalFooter.tsx @@ -8,6 +8,7 @@ export interface ComponentProps { onSubmit: () => void; submitButtonStyleType?: ButtonStyleType; submitButtonType?: ButtonType; + isLoading?: boolean } const ModalFooter: FunctionComponent = ( @@ -23,6 +24,7 @@ const ModalFooter: FunctionComponent = ( onClick={() => { props.onClose && props.onClose(); }} + isLoading={props.isLoading || false} /> ) : ( <> @@ -43,6 +45,7 @@ const ModalFooter: FunctionComponent = ( onClick={() => { props.onSubmit(); }} + isLoading={props.isLoading || false} type={props.submitButtonType ? props.submitButtonType : ButtonType.Button} /> ) : ( diff --git a/CommonUI/src/Components/ModelFormModal/ModelFormModal.tsx b/CommonUI/src/Components/ModelFormModal/ModelFormModal.tsx index bafcad724e..7a84a025c7 100644 --- a/CommonUI/src/Components/ModelFormModal/ModelFormModal.tsx +++ b/CommonUI/src/Components/ModelFormModal/ModelFormModal.tsx @@ -1,15 +1,19 @@ -import React, { ReactElement } from 'react'; +import React, { ReactElement, useRef, useState } from 'react'; import { ButtonStyleType } from '../Button/Button'; import Modal from '../Modal/Modal'; import ModelForm, { ComponentProps as ModelFormComponentProps} from '../Forms/ModelForm'; import BaseModel from 'Common/Models/BaseModel'; import ButtonType from '../Button/ButtonTypes'; +import { JSONObjectOrArray } from 'Common/Types/JSON'; +import { FormikProps, FormikValues } from 'formik'; export interface ComponentProps { title: string; onClose?: () => void; submitButtonText?: string; - onSubmit: () => void; + onSuccess?: ( + data: TBaseModel | JSONObjectOrArray | Array + ) => void; submitButtonStyleType?: ButtonStyleType; formProps: ModelFormComponentProps; } @@ -17,9 +21,21 @@ export interface ComponentProps { const ModelFromModal: Function = ( props: ComponentProps ): ReactElement => { + + const [isLoading, setIsLoading] = useState(false); + const formRef = useRef>(null); + return ( - - {...props.formProps} hideSubmitButton={true}/> + { + formRef.current && formRef.current.handleSubmit(); + }}> + {...props.formProps} hideSubmitButton={true} onLoadingChange={(isFormLoading: boolean) => { setIsLoading(isFormLoading) }} formRef={formRef} onSuccess={ + ( + data: TBaseModel | JSONObjectOrArray | Array + ) => { + props.onSuccess && props.onSuccess(data); + } + } /> ); }; diff --git a/CommonUI/src/Config.ts b/CommonUI/src/Config.ts index 23e3e405d2..1fafc18316 100644 --- a/CommonUI/src/Config.ts +++ b/CommonUI/src/Config.ts @@ -18,7 +18,7 @@ export const IS_SAAS_SERVICE: boolean = env('IS_SAAS_SERVICE') === 'true'; export const DISABLE_SIGNUP: boolean = env('DISABLE_SIGNUP') === 'true'; export const VERSION: Version = new Version(env('VERSION') || '1.0.0'); -export const DASHBOARD_API_ROUTE: Route = new Route(env('DASHBOARD_API_Route')); +export const DASHBOARD_API_ROUTE: Route = new Route(env('DASHBOARD_API_ROUTE')); export const IDENTITY_ROUTE: Route = new Route(env('IDENTITY_ROUTE')); diff --git a/DashboardAPI/Index.ts b/DashboardAPI/Index.ts index 7810e0a6c5..07cb200611 100755 --- a/DashboardAPI/Index.ts +++ b/DashboardAPI/Index.ts @@ -1,42 +1,43 @@ import 'ejs'; import { PostgresAppInstance } from 'CommonServer/Infrastructure/PostgresDatabase'; -import Express, { ExpressApplication } from 'CommonServer/Utils/Express'; +// import Express, { ExpressApplication } from 'CommonServer/Utils/Express'; import logger from 'CommonServer/Utils/Logger'; -import BaseAPI from "CommonServer/API/BaseAPI"; +// import BaseAPI from "CommonServer/API/BaseAPI"; import App from 'CommonServer/Utils/StartServer'; -import User from 'Common/Models/User'; -import UserService, { - Service as UserServiceType, -} from 'CommonServer/Services/UserService'; +// import User from 'Common/Models/User'; +// import UserService, { +// Service as UserServiceType, +// } from 'CommonServer/Services/UserService'; -import Project from 'Common/Models/Project'; -import ProjectService, { - Service as ProjectServiceType, -} from 'CommonServer/Services/ProjectService'; +// import Project from 'Common/Models/Project'; +// import ProjectService, { +// Service as ProjectServiceType, +// } from 'CommonServer/Services/ProjectService'; -import Probe from 'Common/Models/Probe'; -import ProbeService, { - Service as ProbeServiceType, -} from 'CommonServer/Services/ProbeService'; +// import Probe from 'Common/Models/Probe'; +// import ProbeService, { +// Service as ProbeServiceType, +// } from 'CommonServer/Services/ProbeService'; -import EmailVerificationToken from 'Common/Models/EmailVerificationToken'; -import EmailVerificationTokenService, { - Service as EmailVerificationTokenServiceType, -} from 'CommonServer/Services/EmailVerificationTokenService'; +// import EmailVerificationToken from 'Common/Models/EmailVerificationToken'; +// import EmailVerificationTokenService, { +// Service as EmailVerificationTokenServiceType, +// } from 'CommonServer/Services/EmailVerificationTokenService'; -const app: ExpressApplication = Express.getExpressApp(); +// const app: ExpressApplication = Express.getExpressApp(); -const APP_NAME: string = 'dashboard-api'; +const APP_NAME: string = 'api'; -//attach api's -app.use(new User().getCrudApiPath()?.toString()!, new BaseAPI(User, UserService).getRouter()); -app.use(new Project().getCrudApiPath()?.toString()!, new BaseAPI(Project, ProjectService).getRouter()); -app.use(new Probe().getCrudApiPath()?.toString()!, new BaseAPI(Probe, ProbeService).getRouter()); -app.use(new Probe().getCrudApiPath()?.toString()!, new BaseAPI(Probe, ProbeService).getRouter()); -app.use(new EmailVerificationToken().getCrudApiPath()?.toString()!, new BaseAPI(EmailVerificationToken, EmailVerificationTokenService).getRouter()); +// //attach api's +// app.use('/api', new BaseAPI(User, UserService).getRouter()); +// app.use('/api',new BaseAPI(Project, ProjectService).getRouter()); +// app.use('/api',new BaseAPI(Probe, ProbeService).getRouter()); +// app.use('/api',new BaseAPI(Probe, ProbeService).getRouter()); +// app.use('/api',new BaseAPI(EmailVerificationToken, EmailVerificationTokenService).getRouter()); const init: Function = async (): Promise => { + try { // init the app await App(APP_NAME); diff --git a/Nginx/default.conf b/Nginx/default.conf index e8857192df..540c4591dc 100644 --- a/Nginx/default.conf +++ b/Nginx/default.conf @@ -6,6 +6,10 @@ upstream identity { server identity:3087 weight=10 max_fails=3 fail_timeout=30s; } +upstream dashboard-api { + server dashboard-api:3002 weight=10 max_fails=3 fail_timeout=30s; +} + upstream dashboard { server dashboard:3009 weight=10 max_fails=3 fail_timeout=30s; } @@ -65,19 +69,6 @@ server { proxy_pass http://home/; } - location /api { - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # enable WebSockets (for ws://sockjs not connected error in the accounts source: https://stackoverflow.com/questions/41381444/websocket-connection-failed-error-during-websocket-handshake-unexpected-respon) - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_pass http://dashboard-api/; - } - location /accounts { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -116,4 +107,17 @@ server { proxy_set_header Connection "upgrade"; proxy_pass http://identity/; } + + location /api { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # enable WebSockets (for ws://sockjs not connected error in the accounts source: https://stackoverflow.com/questions/41381444/websocket-connection-failed-error-during-websocket-handshake-unexpected-respon) + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_pass http://dashboard-api/; + } } \ No newline at end of file